Ajout exo routage

This commit is contained in:
William Petit 2015-04-09 23:11:56 +02:00 committed by Benjamin Bohard
parent 84381980f7
commit 67b56e88ec
429 changed files with 13323 additions and 47 deletions

View File

@ -0,0 +1,31 @@
# .cadoles-slide-title[Création de directives (1/4)]
- Les directives sont des **composants** réutilisables.
**Création d'une directive basique**
```html
<!-- Fichier index.html -->
<div ng-app="myApp">
<ca-adresse-cadoles></ca-adresse-cadoles>
</div>
<script>
angular.module('myApp', [])
.directive('caAdresseCadoles', function() {
return { // On retourne le descripteur de notre directive
templateUrl: 'adresse-cadoles.html' // ou template: "<div>....</div>"
};
})
;
</script>
<!-- Fichier adresse-cadoles.html -->
<div class="adresse-cadoles">
Cadoles
2 bis, Cours Fleury
21000 DIJON
FRANCE
</div>
```

View File

@ -0,0 +1,26 @@
# .cadoles-slide-title[Création de directives (2/4)]
- Les directives peuvent prendre la forme d'éléments, d'attributs ou de classes.
```html
<script>
angular.module('myApp', [])
.directive('myDirective', function() {
return {
//Type de la directive: 'E' : Element, 'A': Attribut, 'C': Classe (ou composition des trois)
restrict: 'E'
};
})
;
</script>
<!-- Usage -->
<!-- "E" -->
<my-directive></my-directive>
<!-- "A" -->
<div my-directive></div>
<!-- "C" -->
<div class="my-directive"></div>
```

View File

@ -0,0 +1,32 @@
# .cadoles-slide-title[Création de directives (3/4)]
- Ayant leur propre contrôleur, elles peuvent hériter du `$scope` de leur parent ou être en isolation.
- En cas d'isolation, elles peuvent se lier de différentes manières avec le `$scope` de leur parent, notamment
via leurs attributs.
```html
<script>
angular.module('myApp', [])
.directive('helloWho', function() {
return {
restrict: 'E',
template: 'Hello {{ whoOnScope }} !',
scope: {
'whoOnScope': '=whoAttr' // Les attributs sont normalisé, i.e. whoAttr -> "who-attr"
},
controller: function($scope) {
// Faire quelque chose dans le controleur de notre nouvelle directive
}
};
})
;
</script>
<!-- Usage -->
<hello-who who-attr="'World'"></hello-who> <!-- Affiche "Hello World !" -->
```

View File

@ -0,0 +1,31 @@
# .cadoles-slide-title[Création de directives (4/4)]
- Une directive, contrairement à un simple contrôleur peut manipuler le DOM via sa méthode `link()`.
```html
<script>
angular.module('myApp', [])
.directive('myDirective', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
// scope: $scope de notre directive
// element: référence vers l'élément du DOM portant la directive (via wrapper jqLite)
// attrs: objet clé/valeur portant les attributs normalisés de l'élément du DOM
element.text('Hello word !');
// L'évènement $destroy émit par le scope permet
// d'être notifié de la suppression de la directive
scope.$on('$destroy', function() {
// Faire quelque chose à la destruction de la directive
});
}
};
})
;
</script>
```

View File

@ -0,0 +1,41 @@
/*
* Énoncé:
*
* Créer une directive "my-map" qui affiche une carte interactive dans la page via la librairie Leaflet.
* La directive comportera un attribut "marker" qui prendra en paramètre un objet de la forme:
* {
* lat: <latitute>,
* long: <longitude>
* }
* Cet objet devra servir à afficher un "Marker" sur la carte, et centrer celle ci sur la position donnée
*
* ! Le code et la feuille de style de la librairie Leaflet sont déjà intégrés dans la page !
*
* Documentation:
* - Librairie Leaflet: http://leafletjs.com/index.html
*/
var Exo = angular.module('Exo', []);
Exo.directive('myMap', function() {
return {
restrict: 'E',
scope: {
marker: '=marker'
},
link: function(scope, element) {
var map = L.map(element[0]).setView([scope.marker.lat, scope.marker.long], 13);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
L.marker([scope.marker.lat, scope.marker.long]).addTo(map);
}
};
});

View File

@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf8">
<title>Exercice: Création de directive</title>
<link rel="stylesheet" href="../lib/leaflet-0.7.3/leaflet.css"></script>
</head>
<!-- Déclaration de l'application -->
<body ng-app="Exo" ng-init="position = {'lat': 47.32758, long: 5.04277}">
<my-map marker="position" style="display:block; height: 600px; width: 800px;"></my-map>
<!-- Import de du framework Angular -->
<script src="../../node_modules/angular/angular.js"></script>
<script src="../lib/leaflet-0.7.3/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
/*
* Énoncé:
*
* Créer une directive "my-map" qui affiche une carte interactive dans la page via la librairie Leaflet.
* La directive comportera un attribut "marker" qui prendra en paramètre un objet de la forme:
* {
* lat: <latitute>,
* long: <longitude>
* }
* Cet objet devra servir à afficher un "Marker" sur la carte, et centrer celle ci sur la position donnée
*
* ! Le code et la feuille de style de la librairie Leaflet sont déjà intégrés dans la page !
*
* Documentation:
* - Librairie Leaflet: http://leafletjs.com/index.html
*/
var Exo = angular.module('Exo', []);

View File

@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf8">
<title>Exercice: Création de directive</title>
<link rel="stylesheet" href="lib/leaflet-0.7.3/leaflet.css"></script>
</head>
<!-- Déclaration de l'application -->
<body ng-app="Exo" ng-init="position = {'lat': 47.32758, long: 5.04277}">
<my-map marker="position" style="display:block; height: 600px; width: 800px;"></my-map>
<!-- Import de du framework Angular -->
<script src="../node_modules/angular/angular.js"></script>
<script src="lib/leaflet-0.7.3/leaflet.js"></script>
<script src="app.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

View File

@ -0,0 +1,9180 @@
/*
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 oldL = 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') {
module.exports = L;
// 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
L.noConflict = function () {
window.L = oldL;
return this;
};
window.L = L;
/*
* L.Util contains various utility functions used throughout Leaflet code.
*/
L.Util = {
extend: function (dest) { // (Object[, Object, ...]) ->
var sources = Array.prototype.slice.call(arguments, 1),
i, j, len, src;
for (j = 0, len = sources.length; j < len; j++) {
src = sources[j] || {};
for (i in src) {
if (src.hasOwnProperty(i)) {
dest[i] = src[i];
}
}
}
return dest;
},
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);
};
},
stamp: (function () {
var lastId = 0,
key = '_leaflet_id';
return function (obj) {
obj[key] = obj[key] || ++lastId;
return obj[key];
};
}()),
invokeEach: function (obj, method, context) {
var i, args;
if (typeof obj === 'object') {
args = Array.prototype.slice.call(arguments, 3);
for (i in obj) {
method.apply(context, [i, obj[i]].concat(args));
}
return true;
}
return false;
},
limitExecByInterval: function (fn, time, context) {
var lock, execOnUnlock;
return function wrapperFn() {
var args = arguments;
if (lock) {
execOnUnlock = true;
return;
}
lock = true;
setTimeout(function () {
lock = false;
if (execOnUnlock) {
wrapperFn.apply(context, args);
execOnUnlock = false;
}
}, time);
fn.apply(context, args);
};
},
falseFn: function () {
return false;
},
formatNum: function (num, digits) {
var pow = Math.pow(10, digits || 5);
return Math.round(num * pow) / pow;
},
trim: function (str) {
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
},
splitWords: function (str) {
return L.Util.trim(str).split(/\s+/);
},
setOptions: function (obj, options) {
obj.options = L.extend({}, obj.options, options);
return obj.options;
},
getParamString: function (obj, existingUrl, uppercase) {
var params = [];
for (var i in obj) {
params.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));
}
return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');
},
template: function (str, data) {
return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
var value = data[key];
if (value === undefined) {
throw new Error('No value provided for variable ' + str);
} else if (typeof value === 'function') {
value = value(data);
}
return value;
});
},
isArray: Array.isArray || function (obj) {
return (Object.prototype.toString.call(obj) === '[object Array]');
},
emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
};
(function () {
// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/
function getPrefixed(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;
function timeoutDefer(fn) {
var time = +new Date(),
timeToCall = Math.max(0, 16 - (time - lastTime));
lastTime = time + timeToCall;
return window.setTimeout(fn, timeToCall);
}
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, element) {
fn = L.bind(fn, context);
if (immediate && requestFn === timeoutDefer) {
fn();
} else {
return requestFn.call(window, fn, element);
}
};
L.Util.cancelAnimFrame = function (id) {
if (id) {
cancelFn.call(window, id);
}
};
}());
// shortcuts for most used utility functions
L.extend = L.Util.extend;
L.bind = L.Util.bind;
L.stamp = L.Util.stamp;
L.setOptions = L.Util.setOptions;
/*
* L.Class powers the OOP facilities of the library.
* Thanks to John Resig and Dean Edwards for inspiration!
*/
L.Class = function () {};
L.Class.extend = function (props) {
// extended class with the new prototype
var NewClass = function () {
// call the constructor
if (this.initialize) {
this.initialize.apply(this, arguments);
}
// call all constructor hooks
if (this._initHooks) {
this.callInitHooks();
}
};
// instantiate class without calling constructor
var F = function () {};
F.prototype = this.prototype;
var proto = new F();
proto.constructor = NewClass;
NewClass.prototype = proto;
//inherit parent's statics
for (var i in this) {
if (this.hasOwnProperty(i) && i !== 'prototype') {
NewClass[i] = this[i];
}
}
// mix static properties into the class
if (props.statics) {
L.extend(NewClass, props.statics);
delete props.statics;
}
// mix includes into the prototype
if (props.includes) {
L.Util.extend.apply(null, [proto].concat(props.includes));
delete props.includes;
}
// merge options
if (props.options && proto.options) {
props.options = L.extend({}, proto.options, props.options);
}
// mix given properties into the prototype
L.extend(proto, 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 (parent.prototype.callInitHooks) {
parent.prototype.callInitHooks.call(this);
}
this._initHooksCalled = true;
for (var i = 0, len = proto._initHooks.length; i < len; i++) {
proto._initHooks[i].call(this);
}
};
return NewClass;
};
// method for adding properties to prototype
L.Class.include = function (props) {
L.extend(this.prototype, props);
};
// merge new default options to the Class
L.Class.mergeOptions = function (options) {
L.extend(this.prototype.options, options);
};
// add a constructor hook
L.Class.addInitHook = function (fn) { // (Function) || (String, args...)
var args = Array.prototype.slice.call(arguments, 1);
var init = typeof fn === 'function' ? fn : function () {
this[fn].apply(this, args);
};
this.prototype._initHooks = this.prototype._initHooks || [];
this.prototype._initHooks.push(init);
};
/*
* L.Mixin.Events is used to add custom events functionality to Leaflet classes.
*/
var eventsKey = '_leaflet_events';
L.Mixin = {};
L.Mixin.Events = {
addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object])
// types can be a map of types/handlers
if (L.Util.invokeEach(types, this.addEventListener, this, fn, context)) { return this; }
var events = this[eventsKey] = this[eventsKey] || {},
contextId = context && context !== this && L.stamp(context),
i, len, event, type, indexKey, indexLenKey, typeIndex;
// types can be a string of space-separated words
types = L.Util.splitWords(types);
for (i = 0, len = types.length; i < len; i++) {
event = {
action: fn,
context: context || this
};
type = types[i];
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
indexKey = type + '_idx';
indexLenKey = indexKey + '_len';
typeIndex = events[indexKey] = events[indexKey] || {};
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);
} else {
events[type] = events[type] || [];
events[type].push(event);
}
}
return this;
},
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));
},
removeEventListener: function (types, fn, context) { // ([String, Function, Object]) or (Object[, Object])
if (!this[eventsKey]) {
return this;
}
if (!types) {
return this.clearAllEventListeners();
}
if (L.Util.invokeEach(types, this.removeEventListener, this, fn, context)) { return this; }
var events = this[eventsKey],
contextId = context && context !== this && L.stamp(context),
i, len, type, listeners, j, indexKey, indexLenKey, typeIndex, removed;
types = L.Util.splitWords(types);
for (i = 0, len = types.length; i < len; i++) {
type = types[i];
indexKey = type + '_idx';
indexLenKey = indexKey + '_len';
typeIndex = events[indexKey];
if (!fn) {
// clear all listeners for a type if function isn't specified
delete events[type];
delete events[indexKey];
delete events[indexLenKey];
} 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 (context && typeIndex && (listeners.length === 0)) {
delete typeIndex[contextId];
events[indexLenKey]--;
}
}
}
}
return this;
},
clearAllEventListeners: function () {
delete this[eventsKey];
return this;
},
fireEvent: function (type, data) { // (String[, Object])
if (!this.hasEventListeners(type)) {
return this;
}
var event = L.Util.extend({}, data, { type: type, target: this });
var events = this[eventsKey],
listeners, i, len, typeIndex, contextId;
if (events[type]) {
// make sure adding/removing listeners inside other listeners won't cause infinite loop
listeners = events[type].slice();
for (i = 0, len = listeners.length; i < len; i++) {
listeners[i].action.call(listeners[i].context, event);
}
}
// fire event for the context-indexed listeners as well
typeIndex = events[type + '_idx'];
for (contextId in typeIndex) {
listeners = typeIndex[contextId].slice();
if (listeners) {
for (i = 0, len = listeners.length; i < len; i++) {
listeners[i].action.call(listeners[i].context, event);
}
}
}
return this;
},
addOneTimeEventListener: function (types, fn, context) {
if (L.Util.invokeEach(types, this.addOneTimeEventListener, this, fn, context)) { return this; }
var handler = L.bind(function () {
this
.removeEventListener(types, fn, context)
.removeEventListener(types, handler, context);
}, this);
return this
.addEventListener(types, fn, context)
.addEventListener(types, handler, context);
}
};
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;
/*
* L.Browser handles different browser and feature detections for internal Leaflet use.
*/
(function () {
var ie = 'ActiveXObject' in window,
ielt9 = ie && !document.addEventListener,
// 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,
gecko = ua.indexOf('gecko') !== -1,
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,
any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs;
// PhantomJS has 'ontouchstart' in document.documentElement, but doesn't actually support touch.
// https://github.com/Leaflet/Leaflet/pull/1434#issuecomment-13843151
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;
}());
L.Browser = {
ie: ie,
ielt9: ielt9,
webkit: webkit,
gecko: gecko && !webkit && !window.opera && !ie,
android: android,
android23: android23,
chrome: chrome,
ie3d: ie3d,
webkit3d: webkit3d,
gecko3d: gecko3d,
opera3d: opera3d,
any3d: any3d,
mobile: mobile,
mobileWebkit: mobile && webkit,
mobileWebkit3d: mobile && webkit3d,
mobileOpera: mobile && window.opera,
touch: touch,
msPointer: msPointer,
pointer: pointer,
retina: retina
};
}());
/*
* L.Point represents a point with x and y coordinates.
*/
L.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
this.x = (round ? Math.round(x) : x);
this.y = (round ? Math.round(y) : y);
};
L.Point.prototype = {
clone: function () {
return new L.Point(this.x, this.y);
},
// non-destructive, returns a new point
add: function (point) {
return this.clone()._add(L.point(point));
},
// destructive, used directly for performance in situations where it's safe to modify existing point
_add: function (point) {
this.x += point.x;
this.y += point.y;
return this;
},
subtract: function (point) {
return this.clone()._subtract(L.point(point));
},
_subtract: function (point) {
this.x -= point.x;
this.y -= point.y;
return this;
},
divideBy: function (num) {
return this.clone()._divideBy(num);
},
_divideBy: function (num) {
this.x /= num;
this.y /= num;
return this;
},
multiplyBy: function (num) {
return this.clone()._multiplyBy(num);
},
_multiplyBy: function (num) {
this.x *= num;
this.y *= num;
return this;
},
round: function () {
return this.clone()._round();
},
_round: function () {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
},
floor: function () {
return this.clone()._floor();
},
_floor: function () {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
},
distanceTo: function (point) {
point = L.point(point);
var x = point.x - this.x,
y = point.y - this.y;
return Math.sqrt(x * x + y * y);
},
equals: function (point) {
point = L.point(point);
return point.x === this.x &&
point.y === this.y;
},
contains: function (point) {
point = L.point(point);
return Math.abs(point.x) <= Math.abs(this.x) &&
Math.abs(point.y) <= Math.abs(this.y);
},
toString: function () {
return 'Point(' +
L.Util.formatNum(this.x) + ', ' +
L.Util.formatNum(this.y) + ')';
}
};
L.point = function (x, y, round) {
if (x instanceof L.Point) {
return x;
}
if (L.Util.isArray(x)) {
return new L.Point(x[0], x[1]);
}
if (x === undefined || x === null) {
return x;
}
return new L.Point(x, y, round);
};
/*
* L.Bounds represents a rectangular area on the screen in pixel coordinates.
*/
L.Bounds = function (a, b) { //(Point, Point) or Point[]
if (!a) { return; }
var points = b ? [a, b] : a;
for (var i = 0, len = points.length; i < len; i++) {
this.extend(points[i]);
}
};
L.Bounds.prototype = {
// extend the bounds to contain the given point
extend: function (point) { // (Point)
point = L.point(point);
if (!this.min && !this.max) {
this.min = point.clone();
this.max = point.clone();
} else {
this.min.x = Math.min(point.x, this.min.x);
this.max.x = Math.max(point.x, this.max.x);
this.min.y = Math.min(point.y, this.min.y);
this.max.y = Math.max(point.y, this.max.y);
}
return this;
},
getCenter: function (round) { // (Boolean) -> Point
return new L.Point(
(this.min.x + this.max.x) / 2,
(this.min.y + this.max.y) / 2, round);
},
getBottomLeft: function () { // -> Point
return new L.Point(this.min.x, this.max.y);
},
getTopRight: function () { // -> Point
return new L.Point(this.max.x, this.min.y);
},
getSize: function () {
return this.max.subtract(this.min);
},
contains: function (obj) { // (Bounds) or (Point) -> Boolean
var min, max;
if (typeof obj[0] === 'number' || obj instanceof L.Point) {
obj = L.point(obj);
} else {
obj = L.bounds(obj);
}
if (obj instanceof L.Bounds) {
min = obj.min;
max = obj.max;
} else {
min = max = obj;
}
return (min.x >= this.min.x) &&
(max.x <= this.max.x) &&
(min.y >= this.min.y) &&
(max.y <= this.max.y);
},
intersects: function (bounds) { // (Bounds) -> Boolean
bounds = L.bounds(bounds);
var min = this.min,
max = this.max,
min2 = bounds.min,
max2 = bounds.max,
xIntersects = (max2.x >= min.x) && (min2.x <= max.x),
yIntersects = (max2.y >= min.y) && (min2.y <= max.y);
return xIntersects && yIntersects;
},
isValid: function () {
return !!(this.min && this.max);
}
};
L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
if (!a || a instanceof L.Bounds) {
return a;
}
return new L.Bounds(a, b);
};
/*
* L.Transformation is an utility class to perform simple point transformations through a 2d-matrix.
*/
L.Transformation = function (a, b, c, d) {
this._a = a;
this._b = b;
this._c = c;
this._d = d;
};
L.Transformation.prototype = {
transform: function (point, scale) { // (Point, Number) -> Point
return this._transform(point.clone(), scale);
},
// destructive transform (faster)
_transform: function (point, scale) {
scale = scale || 1;
point.x = scale * (this._a * point.x + this._b);
point.y = scale * (this._c * point.y + this._d);
return point;
},
untransform: function (point, scale) {
scale = scale || 1;
return new L.Point(
(point.x / scale - this._b) / this._a,
(point.y / scale - this._d) / this._c);
}
};
/*
* L.DomUtil contains various utility functions for working with DOM.
*/
L.DomUtil = {
get: function (id) {
return (typeof id === 'string' ? document.getElementById(id) : id);
},
getStyle: function (el, 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);
value = css ? css[style] : null;
}
return value === 'auto' ? null : value;
},
getViewportOffset: function (element) {
var top = 0,
left = 0,
el = element,
docBody = document.body,
docEl = document.documentElement,
pos;
do {
top += el.offsetTop || 0;
left += el.offsetLeft || 0;
//add borders
top += parseInt(L.DomUtil.getStyle(el, 'borderTopWidth'), 10) || 0;
left += parseInt(L.DomUtil.getStyle(el, 'borderLeftWidth'), 10) || 0;
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);
},
documentIsLtr: function () {
if (!L.DomUtil._docIsLtrCached) {
L.DomUtil._docIsLtrCached = true;
L.DomUtil._docIsLtr = L.DomUtil.getStyle(document.body, 'direction') === 'ltr';
}
return L.DomUtil._docIsLtr;
},
create: function (tagName, className, container) {
var el = document.createElement(tagName);
el.className = className;
if (container) {
container.appendChild(el);
}
return el;
},
hasClass: function (el, name) {
if (el.classList !== undefined) {
return el.classList.contains(name);
}
var className = L.DomUtil._getClass(el);
return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
},
addClass: function (el, name) {
if (el.classList !== undefined) {
var classes = L.Util.splitWords(name);
for (var i = 0, len = classes.length; i < len; i++) {
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);
}
},
removeClass: function (el, name) {
if (el.classList !== undefined) {
el.classList.remove(name);
} else {
L.DomUtil._setClass(el, L.Util.trim((' ' + L.DomUtil._getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
}
},
_setClass: function (el, name) {
if (el.className.baseVal === undefined) {
el.className = name;
} else {
// in case of SVG element
el.className.baseVal = name;
}
},
_getClass: function (el) {
return el.className.baseVal === undefined ? el.className : el.className.baseVal;
},
setOpacity: function (el, value) {
if ('opacity' in el.style) {
el.style.opacity = value;
} else if ('filter' in el.style) {
var filter = false,
filterName = 'DXImageTransform.Microsoft.Alpha';
// filters collection throws an error if we try to retrieve a filter that doesn't exist
try {
filter = el.filters.item(filterName);
} catch (e) {
// don't set opacity to 1 if we haven't already set an opacity,
// it isn't needed and breaks transparent pngs.
if (value === 1) { return; }
}
value = Math.round(value * 100);
if (filter) {
filter.Enabled = (value !== 100);
filter.Opacity = value;
} else {
el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
}
}
},
testProp: function (props) {
var style = document.documentElement.style;
for (var i = 0; i < props.length; i++) {
if (props[i] in style) {
return props[i];
}
}
return false;
},
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) {
var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))),
scaleStr = ' scale(' + scale + ') ';
return preTranslateStr + scaleStr;
},
setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean])
// jshint camelcase: false
el._leaflet_pos = 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';
}
},
getPosition: function (el) {
// this method is only used for elements previously positioned using setPosition,
// so it's safe to cache the position for performance
// jshint camelcase: false
return el._leaflet_pos;
}
};
// prefix style property names
L.DomUtil.TRANSFORM = L.DomUtil.testProp(
['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);
// 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 = L.DomUtil.testProp(
['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);
L.DomUtil.TRANSITION_END =
L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ?
L.DomUtil.TRANSITION + 'End' : 'transitionend';
(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);
},
enableImageDrag: function () {
L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault);
}
});
})();
/*
* L.LatLng represents a geographical point with latitude and longitude coordinates.
*/
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;
if (alt !== undefined) {
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) { // (LatLng) -> Boolean
if (!obj) { return false; }
obj = L.latLng(obj);
var margin = Math.max(
Math.abs(this.lat - obj.lat),
Math.abs(this.lng - obj.lng));
return margin <= L.LatLng.MAX_MARGIN;
},
toString: function (precision) { // (Number) -> String
return 'LatLng(' +
L.Util.formatNum(this.lat, precision) + ', ' +
L.Util.formatNum(this.lng, precision) + ')';
},
// 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 (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);
return new L.LatLng(this.lat, lng);
}
};
L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Number)
if (a instanceof L.LatLng) {
return a;
}
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;
}
}
if (a === undefined || a === null) {
return a;
}
if (typeof a === 'object' && 'lat' in a) {
return new L.LatLng(a.lat, 'lng' in a ? a.lng : a.lon);
}
if (b === undefined) {
return null;
}
return new L.LatLng(a, b);
};
/*
* L.LatLngBounds represents a rectangular area on the map in geographical coordinates.
*/
L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLng[])
if (!southWest) { return; }
var latlngs = northEast ? [southWest, northEast] : southWest;
for (var i = 0, len = latlngs.length; i < len; i++) {
this.extend(latlngs[i]);
}
};
L.LatLngBounds.prototype = {
// extend the bounds to contain the given point or bounds
extend: function (obj) { // (LatLng) or (LatLngBounds)
if (!obj) { return this; }
var latLng = L.latLng(obj);
if (latLng !== null) {
obj = latLng;
} else {
obj = L.latLngBounds(obj);
}
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;
},
// extend the bounds by a percentage
pad: function (bufferRatio) { // (Number) -> LatLngBounds
var sw = this._southWest,
ne = this._northEast,
heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,
widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;
return new L.LatLngBounds(
new L.LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),
new L.LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));
},
getCenter: function () { // -> LatLng
return new L.LatLng(
(this._southWest.lat + this._northEast.lat) / 2,
(this._southWest.lng + this._northEast.lng) / 2);
},
getSouthWest: function () {
return this._southWest;
},
getNorthEast: function () {
return this._northEast;
},
getNorthWest: function () {
return new L.LatLng(this.getNorth(), this.getWest());
},
getSouthEast: function () {
return new L.LatLng(this.getSouth(), this.getEast());
},
getWest: function () {
return this._southWest.lng;
},
getSouth: function () {
return this._southWest.lat;
},
getEast: function () {
return this._northEast.lng;
},
getNorth: function () {
return this._northEast.lat;
},
contains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean
if (typeof obj[0] === 'number' || obj instanceof L.LatLng) {
obj = L.latLng(obj);
} else {
obj = L.latLngBounds(obj);
}
var sw = this._southWest,
ne = this._northEast,
sw2, ne2;
if (obj instanceof L.LatLngBounds) {
sw2 = obj.getSouthWest();
ne2 = obj.getNorthEast();
} else {
sw2 = ne2 = obj;
}
return (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&
(sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);
},
intersects: function (bounds) { // (LatLngBounds)
bounds = L.latLngBounds(bounds);
var sw = this._southWest,
ne = this._northEast,
sw2 = bounds.getSouthWest(),
ne2 = bounds.getNorthEast(),
latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),
lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);
return latIntersects && lngIntersects;
},
toBBoxString: function () {
return [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');
},
equals: function (bounds) { // (LatLngBounds)
if (!bounds) { return false; }
bounds = L.latLngBounds(bounds);
return this._southWest.equals(bounds.getSouthWest()) &&
this._northEast.equals(bounds.getNorthEast());
},
isValid: function () {
return !!(this._southWest && this._northEast);
}
};
//TODO International date line?
L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng)
if (!a || a instanceof L.LatLngBounds) {
return a;
}
return new L.LatLngBounds(a, b);
};
/*
* L.Projection contains various geographical projections used by CRS classes.
*/
L.Projection = {};
/*
* Spherical Mercator is the most popular map projection, used by EPSG:3857 CRS used by default.
*/
L.Projection.SphericalMercator = {
MAX_LATITUDE: 85.0511287798,
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;
y = Math.log(Math.tan((Math.PI / 4) + (y / 2)));
return new L.Point(x, y);
},
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(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);
},
unproject: function (point) {
return new L.LatLng(point.y, point.x);
}
};
/*
* L.CRS is a base object for all defined CRS (Coordinate Reference Systems) in Leaflet.
*/
L.CRS = {
latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point
var projectedPoint = this.projection.project(latlng),
scale = this.scale(zoom);
return this.transformation._transform(projectedPoint, scale);
},
pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng
var scale = this.scale(zoom),
untransformedPoint = this.transformation.untransform(point, scale);
return this.projection.unproject(untransformedPoint);
},
project: function (latlng) {
return this.projection.project(latlng);
},
scale: function (zoom) {
return 256 * Math.pow(2, zoom);
},
getSize: function (zoom) {
var s = this.scale(zoom);
return L.point(s, s);
}
};
/*
* A simple CRS that can be used for flat non-Earth maps like panoramas or game maps.
*/
L.CRS.Simple = L.extend({}, L.CRS, {
projection: L.Projection.LonLat,
transformation: new L.Transformation(1, 0, -1, 0),
scale: function (zoom) {
return Math.pow(2, zoom);
}
});
/*
* 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, {
code: 'EPSG:3857',
projection: L.Projection.SphericalMercator,
transformation: new L.Transformation(0.5 / Math.PI, 0.5, -0.5 / Math.PI, 0.5),
project: function (latlng) { // (LatLng) -> Point
var projectedPoint = this.projection.project(latlng),
earthRadius = 6378137;
return projectedPoint.multiplyBy(earthRadius);
}
});
L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, {
code: 'EPSG:900913'
});
/*
* L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists.
*/
L.CRS.EPSG4326 = L.extend({}, L.CRS, {
code: 'EPSG:4326',
projection: L.Projection.LonLat,
transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5)
});
/*
* L.Map is the central class of the API - it is used to create a map.
*/
L.Map = L.Class.extend({
includes: L.Mixin.Events,
options: {
crs: L.CRS.EPSG3857,
/*
center: LatLng,
zoom: Number,
layers: Array,
*/
fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23,
trackResize: 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();
// hack for https://github.com/Leaflet/Leaflet/issues/1980
this._onResize = L.bind(this._onResize, this);
this._initEvents();
if (options.maxBounds) {
this.setMaxBounds(options.maxBounds);
}
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(options.layers);
},
// public methods that modify map state
// replaced by animation-powered implementation in Map.PanAnimation.js
setView: function (center, zoom) {
zoom = zoom === undefined ? this.getZoom() : zoom;
this._resetView(L.latLng(center), this._limitZoom(zoom));
return this;
},
setZoom: function (zoom, options) {
if (!this._loaded) {
this._zoom = this._limitZoom(zoom);
return this;
}
return this.setView(this.getCenter(), zoom, {zoom: options});
},
zoomIn: function (delta, options) {
return this.setZoom(this._zoom + (delta || 1), options);
},
zoomOut: function (delta, options) {
return this.setZoom(this._zoom - (delta || 1), options);
},
setZoomAround: function (latlng, zoom, options) {
var scale = this.getZoomScale(zoom),
viewHalf = this.getSize().divideBy(2),
containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng),
centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale),
newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
return this.setView(newCenter, zoom, {zoom: options});
},
fitBounds: function (bounds, options) {
options = options || {};
bounds = bounds.getBounds ? bounds.getBounds() : L.latLngBounds(bounds);
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)),
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);
},
fitWorld: function (options) {
return this.fitBounds([[-90, -180], [90, 180]], options);
},
panTo: function (center, options) { // (LatLng)
return this.setView(center, this._zoom, {pan: options});
},
panBy: function (offset) { // (Point)
// replaced with animated panBy in Map.PanAnimation.js
this.fire('movestart');
this._rawPanBy(L.point(offset));
this.fire('move');
return this.fire('moveend');
},
setMaxBounds: function (bounds) {
bounds = L.latLngBounds(bounds);
this.options.maxBounds = bounds;
if (!bounds) {
return this.off('moveend', this._panInsideMaxBounds, this);
}
if (this._loaded) {
this._panInsideMaxBounds();
}
return this.on('moveend', this._panInsideMaxBounds, this);
},
panInsideBounds: function (bounds, options) {
var center = this.getCenter(),
newCenter = this._limitCenter(center, this._zoom, bounds);
if (center.equals(newCenter)) { return this; }
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; }
options = L.extend({
animate: false,
pan: true
}, options === true ? {animate: true} : options);
var oldSize = this.getSize();
this._sizeChanged = true;
this._initialCenter = null;
var newSize = this.getSize(),
oldCenter = oldSize.divideBy(2).round(),
newCenter = newSize.divideBy(2).round(),
offset = oldCenter.subtract(newCenter);
if (!offset.x && !offset.y) { return this; }
if (options.animate && options.pan) {
this.panBy(offset);
} else {
if (options.pan) {
this._rawPanBy(offset);
}
this.fire('move');
if (options.debounceMoveend) {
clearTimeout(this._sizeTimer);
this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200);
} else {
this.fire('moveend');
}
}
return this.fire('resize', {
oldSize: oldSize,
newSize: newSize
});
},
// TODO handler.addTo
addHandler: function (name, HandlerClass) {
if (!HandlerClass) { return this; }
var handler = this[name] = new HandlerClass(this);
this._handlers.push(handler);
if (this.options[name]) {
handler.enable();
}
return this;
},
remove: function () {
if (this._loaded) {
this.fire('unload');
}
this._initEvents('off');
try {
// throws error in IE6-8
delete this._container._leaflet;
} catch (e) {
this._container._leaflet = undefined;
}
this._clearPanes();
if (this._clearControlPos) {
this._clearControlPos();
}
this._clearHandlers();
return this;
},
// public methods for getting map state
getCenter: function () { // (Boolean) -> LatLng
this._checkIfLoaded();
if (this._initialCenter && !this._moved()) {
return this._initialCenter;
}
return this.layerPointToLatLng(this._getCenterLayerPoint());
},
getZoom: function () {
return this._zoom;
},
getBounds: function () {
var bounds = this.getPixelBounds(),
sw = this.unproject(bounds.getBottomLeft()),
ne = this.unproject(bounds.getTopRight());
return new L.LatLngBounds(sw, ne);
},
getMinZoom: function () {
return this.options.minZoom === undefined ?
(this._layersMinZoom === undefined ? 0 : this._layersMinZoom) :
this.options.minZoom;
},
getMaxZoom: function () {
return this.options.maxZoom === undefined ?
(this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom) :
this.options.maxZoom;
},
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
bounds = L.latLngBounds(bounds);
var zoom = this.getMinZoom() - (inside ? 1 : 0),
maxZoom = this.getMaxZoom(),
size = this.getSize(),
nw = bounds.getNorthWest(),
se = bounds.getSouthEast(),
zoomNotFound = true,
boundsSize;
padding = L.point(padding || [0, 0]);
do {
zoom++;
boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding);
zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y;
} while (zoomNotFound && zoom <= maxZoom);
if (zoomNotFound && inside) {
return null;
}
return inside ? zoom : zoom - 1;
},
getSize: function () {
if (!this._size || this._sizeChanged) {
this._size = new L.Point(
this._container.clientWidth,
this._container.clientHeight);
this._sizeChanged = false;
}
return this._size.clone();
},
getPixelBounds: function () {
var topLeftPoint = this._getTopLeftPoint();
return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
},
getPixelOrigin: function () {
this._checkIfLoaded();
return this._initialTopLeftPoint;
},
getPanes: function () {
return this._panes;
},
getContainer: function () {
return this._container;
},
// TODO replace with universal implementation after refactoring projections
getZoomScale: function (toZoom) {
var crs = this.options.crs;
return crs.scale(toZoom) / crs.scale(this._zoom);
},
getScaleZoom: function (scale) {
return this._zoom + (Math.log(scale) / Math.LN2);
},
// conversion methods
project: function (latlng, zoom) { // (LatLng[, Number]) -> Point
zoom = zoom === undefined ? this._zoom : zoom;
return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);
},
unproject: function (point, zoom) { // (Point[, Number]) -> LatLng
zoom = zoom === undefined ? this._zoom : zoom;
return this.options.crs.pointToLatLng(L.point(point), zoom);
},
layerPointToLatLng: function (point) { // (Point)
var projectedPoint = L.point(point).add(this.getPixelOrigin());
return this.unproject(projectedPoint);
},
latLngToLayerPoint: function (latlng) { // (LatLng)
var projectedPoint = this.project(L.latLng(latlng))._round();
return projectedPoint._subtract(this.getPixelOrigin());
},
containerPointToLayerPoint: function (point) { // (Point)
return L.point(point).subtract(this._getMapPanePos());
},
layerPointToContainerPoint: function (point) { // (Point)
return L.point(point).add(this._getMapPanePos());
},
containerPointToLatLng: function (point) {
var layerPoint = this.containerPointToLayerPoint(L.point(point));
return this.layerPointToLatLng(layerPoint);
},
latLngToContainerPoint: function (latlng) {
return this.layerPointToContainerPoint(this.latLngToLayerPoint(L.latLng(latlng)));
},
mouseEventToContainerPoint: function (e) { // (MouseEvent)
return L.DomEvent.getMousePosition(e, this._container);
},
mouseEventToLayerPoint: function (e) { // (MouseEvent)
return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
},
mouseEventToLatLng: function (e) { // (MouseEvent)
return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
},
// map initialization methods
_initContainer: function (id) {
var container = this._container = L.DomUtil.get(id);
if (!container) {
throw new Error('Map container not found.');
} else if (container._leaflet) {
throw new Error('Map container is already initialized.');
}
container._leaflet = true;
},
_initLayout: function () {
var container = this._container;
L.DomUtil.addClass(container, 'leaflet-container' +
(L.Browser.touch ? ' leaflet-touch' : '') +
(L.Browser.retina ? ' leaflet-retina' : '') +
(L.Browser.ielt9 ? ' leaflet-oldie' : '') +
(this.options.fadeAnimation ? ' leaflet-fade-anim' : ''));
var position = L.DomUtil.getStyle(container, 'position');
if (position !== 'absolute' && position !== 'relative' && position !== 'fixed') {
container.style.position = 'relative';
}
this._initPanes();
if (this._initControlPos) {
this._initControlPos();
}
},
_initPanes: function () {
var panes = this._panes = {};
this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container);
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, 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]);
}
},
// private methods that modify map state
_resetView: function (center, zoom, preserveMapOffset, afterZoomAnim) {
var zoomChanged = (this._zoom !== zoom);
if (!afterZoomAnim) {
this.fire('movestart');
if (zoomChanged) {
this.fire('zoomstart');
}
}
this._zoom = zoom;
this._initialCenter = center;
this._initialTopLeftPoint = this._getNewTopLeftPoint(center);
if (!preserveMapOffset) {
L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
} else {
this._initialTopLeftPoint._add(this._getMapPanePos());
}
this._tileLayersToLoad = this._tileLayersNum;
var loading = !this._loaded;
this._loaded = true;
this.fire('viewreset', {hard: !preserveMapOffset});
if (loading) {
this.fire('load');
this.eachLayer(this._layerAdd, this);
}
this.fire('move');
if (zoomChanged || afterZoomAnim) {
this.fire('zoomend');
}
this.fire('moveend', {hard: !preserveMapOffset});
},
_rawPanBy: function (offset) {
L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
},
_getZoomSpan: function () {
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);
},
_checkIfLoaded: function () {
if (!this._loaded) {
throw new Error('Set map center and zoom first.');
}
},
// map events
_initEvents: function (onOff) {
if (!L.DomEvent) { return; }
onOff = onOff || 'on';
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);
}
},
_onResize: function () {
L.Util.cancelAnimFrame(this._resizeRequest);
this._resizeRequest = L.Util.requestAnimFrame(
function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container);
},
_onMouseClick: function (e) {
if (!this._loaded || (!e._simulated &&
((this.dragging && this.dragging.moved()) ||
(this.boxZoom && this.boxZoom.moved()))) ||
L.DomEvent._skipped(e)) { return; }
this.fire('preclick');
this._fireMouseEvent(e);
},
_fireMouseEvent: function (e) {
if (!this._loaded || L.DomEvent._skipped(e)) { return; }
var type = e.type;
type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
if (!this.hasEventListeners(type)) { return; }
if (type === 'contextmenu') {
L.DomEvent.preventDefault(e);
}
var containerPoint = this.mouseEventToContainerPoint(e),
layerPoint = this.containerPointToLayerPoint(containerPoint),
latlng = this.layerPointToLatLng(layerPoint);
this.fire(type, {
latlng: latlng,
layerPoint: layerPoint,
containerPoint: containerPoint,
originalEvent: e
});
},
_onTileLayerLoad: function () {
this._tileLayersToLoad--;
if (this._tileLayersNum && !this._tileLayersToLoad) {
this.fire('tilelayersload');
}
},
_clearHandlers: function () {
for (var i = 0, len = this._handlers.length; i < len; i++) {
this._handlers[i].disable();
}
},
whenReady: function (callback, context) {
if (this._loaded) {
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
_getMapPanePos: function () {
return L.DomUtil.getPosition(this._mapPane);
},
_moved: function () {
var pos = this._getMapPanePos();
return pos && !pos.equals([0, 0]);
},
_getTopLeftPoint: function () {
return this.getPixelOrigin().subtract(this._getMapPanePos());
},
_getNewTopLeftPoint: function (center, zoom) {
var viewHalf = this.getSize()._divideBy(2);
// TODO round on display, not calculation to increase precision?
return this.project(center, zoom)._subtract(viewHalf)._round();
},
_latLngToNewLayerPoint: function (latlng, newZoom, newCenter) {
var topLeft = this._getNewTopLeftPoint(newCenter, newZoom).add(this._getMapPanePos());
return this.project(latlng, newZoom)._subtract(topLeft);
},
// layer point of the current center
_getCenterLayerPoint: function () {
return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
},
// offset of the specified place to the current center in pixels
_getCenterOffset: function (latlng) {
return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
},
// adjust center for view to get inside bounds
_limitCenter: function (center, zoom, bounds) {
if (!bounds) { return center; }
var centerPoint = this.project(center, zoom),
viewHalf = this.getSize().divideBy(2),
viewBounds = new L.Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
offset = this._getBoundsOffset(viewBounds, bounds, zoom);
return this.unproject(centerPoint.add(offset), zoom);
},
// adjust offset for view to get inside bounds
_limitOffset: function (offset, bounds) {
if (!bounds) { return offset; }
var viewBounds = this.getPixelBounds(),
newBounds = new L.Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
return offset.add(this._getBoundsOffset(newBounds, bounds));
},
// returns offset needed for pxBounds to get inside maxBounds at a specified zoom
_getBoundsOffset: function (pxBounds, maxBounds, zoom) {
var nwOffset = this.project(maxBounds.getNorthWest(), zoom).subtract(pxBounds.min),
seOffset = this.project(maxBounds.getSouthEast(), zoom).subtract(pxBounds.max),
dx = this._rebound(nwOffset.x, -seOffset.x),
dy = this._rebound(nwOffset.y, -seOffset.y);
return new L.Point(dx, dy);
},
_rebound: function (left, right) {
return left + right > 0 ?
Math.round(left - right) / 2 :
Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
},
_limitZoom: function (zoom) {
var min = this.getMinZoom(),
max = this.getMaxZoom();
return Math.max(min, Math.min(max, zoom));
}
});
L.map = function (id, options) {
return new L.Map(id, options);
};
/*
* 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 = {
MAX_LATITUDE: 85.0840591556,
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, lng);
}
};
L.CRS.EPSG3395 = L.extend({}, L.CRS, {
code: 'EPSG:3395',
projection: L.Projection.Mercator,
transformation: (function () {
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.TileLayer is used for standard xyz-numbered tile layers.
*/
L.TileLayer = L.Class.extend({
includes: L.Mixin.Events,
options: {
minZoom: 0,
maxZoom: 18,
tileSize: 256,
subdomains: 'abc',
errorTileUrl: '',
attribution: '',
zoomOffset: 0,
opacity: 1,
/*
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) {
options = L.setOptions(this, options);
// detecting retina displays, adjusting tileSize and zoom levels
if (options.detectRetina && L.Browser.retina && options.maxZoom > 0) {
options.tileSize = Math.floor(options.tileSize / 2);
options.zoomOffset++;
if (options.minZoom > 0) {
options.minZoom--;
}
this.options.maxZoom--;
}
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) {
this._url = url;
if (!noRedraw) {
this.redraw();
}
return this;
},
redraw: function () {
if (this._map) {
this._reset({hard: true});
this._update();
}
return this;
},
_updateZIndex: function () {
if (this._container && this.options.zIndex !== undefined) {
this._container.style.zIndex = this.options.zIndex;
}
},
_setAutoZIndex: function (pane, compare) {
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);
},
_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);
}
},
_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();
}
}
},
_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();
}
this._initContainer();
},
_getTileSize: function () {
var map = this._map,
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 () {
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];
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/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 () {
var options = this.options,
zoom = this._map.getZoom();
if (options.zoomReverse) {
zoom = options.maxZoom - zoom;
}
zoom += options.zoomOffset;
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];
},
_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');
}
if (!this._tilesToLoad) {
this.fire('load');
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();
}
});
L.tileLayer = function (url, options) {
return new L.TileLayer(url, options);
};
/*
* L.TileLayer.WMS is used for putting WMS tile layers on the map.
*/
L.TileLayer.WMS = L.TileLayer.extend({
defaultWmsParams: {
service: 'WMS',
request: 'GetMap',
version: '1.1.1',
layers: '',
styles: '',
format: 'image/jpeg',
transparent: false
},
initialize: function (url, options) { // (String, Object)
this._url = url;
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;
}
for (var i in options) {
// all keys that are not TileLayer options go to WMS params
if (!this.options.hasOwnProperty(i) && i !== 'crs') {
wmsParams[i] = options[i];
}
}
this.wmsParams = wmsParams;
L.setOptions(this, options);
},
onAdd: function (map) {
this._crs = this.options.crs || map.options.crs;
this._wmsVersion = parseFloat(this.wmsParams.version);
var projectionKey = this._wmsVersion >= 1.3 ? 'crs' : 'srs';
this.wmsParams[projectionKey] = this._crs.code;
L.TileLayer.prototype.onAdd.call(this, map);
},
getTileUrl: function (tilePoint) { // (Point, Number) -> String
var map = this._map,
tileSize = this.options.tileSize,
nwPoint = tilePoint.multiplyBy(tileSize),
sePoint = nwPoint.add([tileSize, tileSize]),
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.Util.template(this._url, {s: this._getSubdomain(tilePoint)});
return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox;
},
setParams: function (params, noRedraw) {
L.extend(this.wmsParams, params);
if (!noRedraw) {
this.redraw();
}
return this;
}
});
L.tileLayer.wms = function (url, options) {
return new L.TileLayer.WMS(url, options);
};
/*
* L.TileLayer.Canvas is a class that you can use as a base for creating
* dynamically drawn Canvas-based tile layers.
*/
L.TileLayer.Canvas = L.TileLayer.extend({
options: {
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)
this._url = url;
this._bounds = L.latLngBounds(bounds);
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map;
if (!this._image) {
this._initImage();
}
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 (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;
this._updateOpacity();
return this;
},
// TODO remove bringToFront/bringToBack duplication from TileLayer/Path
bringToFront: function () {
if (this._image) {
this._map._panes.overlayPane.appendChild(this._image);
}
return this;
},
bringToBack: function () {
var pane = this._map._panes.overlayPane;
if (this._image) {
pane.insertBefore(this._image, pane.firstChild);
}
return this;
},
setUrl: function (url) {
this._url = url;
this._image.src = this._url;
},
getAttribution: function () {
return this.options.attribution;
},
_initImage: function () {
this._image = L.DomUtil.create('img', 'leaflet-image-layer');
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');
}
this._updateOpacity();
//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 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)));
image.style[L.DomUtil.TRANSFORM] =
L.DomUtil.getTranslateString(origin) + ' scale(' + scale + ') ';
},
_reset: function () {
var image = this._image,
topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()),
size = this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(topLeft);
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);
}
});
L.imageOverlay = function (url, bounds, options) {
return new L.ImageOverlay(url, bounds, options);
};
/*
* L.Icon is an image-based icon class that you can use with L.Marker for custom markers.
*/
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)
iconAnchor: (Point) (centered by default, can be set in CSS with negative margins)
popupAnchor: (Point) (if not specified, popup opens in the anchor point)
shadowUrl: (String) (no shadow by default)
shadowRetinaUrl: (String) (optional, used for retina devices if detected)
shadowSize: (Point)
shadowAnchor: (Point)
*/
className: ''
},
initialize: function (options) {
L.setOptions(this, options);
},
createIcon: function (oldIcon) {
return this._createIcon('icon', oldIcon);
},
createShadow: function (oldIcon) {
return this._createIcon('shadow', oldIcon);
},
_createIcon: function (name, oldIcon) {
var src = this._getIconUrl(name);
if (!src) {
if (name === 'icon') {
throw new Error('iconUrl not set in Icon options (see the docs).');
}
return null;
}
var img;
if (!oldIcon || oldIcon.tagName !== 'IMG') {
img = this._createImg(src);
} else {
img = this._createImg(src, oldIcon);
}
this._setIconStyles(img, name);
return img;
},
_setIconStyles: function (img, name) {
var options = this.options,
size = L.point(options[name + 'Size']),
anchor;
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';
img.style.marginTop = (-anchor.y) + 'px';
}
if (size) {
img.style.width = size.x + 'px';
img.style.height = size.y + 'px';
}
},
_createImg: function (src, el) {
el = el || document.createElement('img');
el.src = src;
return el;
},
_getIconUrl: function (name) {
if (L.Browser.retina && this.options[name + 'RetinaUrl']) {
return this.options[name + 'RetinaUrl'];
}
return this.options[name + 'Url'];
}
});
L.icon = function (options) {
return new L.Icon(options);
};
/*
* L.Icon.Default is the blue marker icon used by default in Leaflet.
*/
L.Icon.Default = L.Icon.extend({
options: {
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
},
_getIconUrl: function (name) {
var key = name + 'Url';
if (this.options[key]) {
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 + '.png';
}
});
L.Icon.Default.imagePath = (function () {
var scripts = document.getElementsByTagName('script'),
leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/;
var i, len, src, matches, path;
for (i = 0, len = scripts.length; i < len; i++) {
src = scripts[i].src;
matches = src.match(leafletRe);
if (matches) {
path = src.split(leafletRe)[0];
return (path ? path + '/' : '') + 'images';
}
}
}());
/*
* L.Marker is used to display clickable/draggable icons on the map.
*/
L.Marker = L.Class.extend({
includes: L.Mixin.Events,
options: {
icon: new L.Icon.Default(),
title: '',
alt: '',
clickable: true,
draggable: false,
keyboard: true,
zIndexOffset: 0,
opacity: 1,
riseOnHover: false,
riseOffset: 250
},
initialize: function (latlng, options) {
L.setOptions(this, options);
this._latlng = L.latLng(latlng);
},
onAdd: function (map) {
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 (map) {
if (this.dragging) {
this.dragging.disable();
}
this._removeIcon();
this._removeShadow();
this.fire('remove');
map.off({
'viewreset': this.update,
'zoomanim': this._animateZoom
}, this);
this._map = null;
},
getLatLng: function () {
return this._latlng;
},
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
this.update();
return this.fire('move', { latlng: this._latlng });
},
setZIndexOffset: function (offset) {
this.options.zIndexOffset = offset;
this.update();
return this;
},
setIcon: function (icon) {
this.options.icon = icon;
if (this._map) {
this._initIcon();
this.update();
}
if (this._popup) {
this.bindPopup(this._popup);
}
return this;
},
update: function () {
if (this._icon) {
var pos = this._map.latLngToLayerPoint(this._latlng).round();
this._setPos(pos);
}
return this;
},
_initIcon: function () {
var options = this.options,
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;
// if we're not reusing the icon, remove the old one and init new one
if (icon !== this._icon) {
if (this._icon) {
this._removeIcon();
}
addIcon = true;
if (options.title) {
icon.title = options.title;
}
if (options.alt) {
icon.alt = options.alt;
}
}
L.DomUtil.addClass(icon, classToAdd);
if (options.keyboard) {
icon.tabIndex = '0';
}
this._icon = icon;
this._initInteraction();
if (options.riseOnHover) {
L.DomEvent
.on(icon, 'mouseover', this._bringToFront, this)
.on(icon, 'mouseout', this._resetZIndex, this);
}
var newShadow = options.icon.createShadow(this._shadow),
addShadow = false;
if (newShadow !== this._shadow) {
this._removeShadow();
addShadow = true;
}
if (newShadow) {
L.DomUtil.addClass(newShadow, classToAdd);
}
this._shadow = newShadow;
if (options.opacity < 1) {
this._updateOpacity();
}
var panes = this._map._panes;
if (addIcon) {
panes.markerPane.appendChild(this._icon);
}
if (newShadow && addShadow) {
panes.shadowPane.appendChild(this._shadow);
}
},
_removeIcon: function () {
if (this.options.riseOnHover) {
L.DomEvent
.off(this._icon, 'mouseover', this._bringToFront)
.off(this._icon, 'mouseout', this._resetZIndex);
}
this._map._panes.markerPane.removeChild(this._icon);
this._icon = null;
},
_removeShadow: function () {
if (this._shadow) {
this._map._panes.shadowPane.removeChild(this._shadow);
}
this._shadow = null;
},
_setPos: function (pos) {
L.DomUtil.setPosition(this._icon, pos);
if (this._shadow) {
L.DomUtil.setPosition(this._shadow, pos);
}
this._zIndex = pos.y + this.options.zIndexOffset;
this._resetZIndex();
},
_updateZIndex: function (offset) {
this._icon.style.zIndex = this._zIndex + offset;
},
_animateZoom: function (opt) {
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();
this._setPos(pos);
},
_initInteraction: function () {
if (!this.options.clickable) { return; }
// TODO refactor into something shared with Map/Path/etc. to DRY it up
var icon = this._icon,
events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'contextmenu'];
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) {
this.dragging = new L.Handler.MarkerDrag(this);
if (this.options.draggable) {
this.dragging.enable();
}
}
},
_onMouseClick: function (e) {
var wasDragged = this.dragging && this.dragging.moved();
if (this.hasEventListeners(e.type) || wasDragged) {
L.DomEvent.stopPropagation(e);
}
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
});
}
},
_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);
}
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
if (this._map) {
this._updateOpacity();
}
return this;
},
_updateOpacity: function () {
L.DomUtil.setOpacity(this._icon, this.options.opacity);
if (this._shadow) {
L.DomUtil.setOpacity(this._shadow, this.options.opacity);
}
},
_bringToFront: function () {
this._updateZIndex(this.options.riseOffset);
},
_resetZIndex: function () {
this._updateZIndex(0);
}
});
L.marker = function (latlng, options) {
return new L.Marker(latlng, options);
};
/*
* L.DivIcon is a lightweight HTML-based icon class (as opposed to the image-based L.Icon)
* to use with L.Marker.
*/
L.DivIcon = L.Icon.extend({
options: {
iconSize: [12, 12], // also can be set through CSS
/*
iconAnchor: (Point)
popupAnchor: (Point)
html: (String)
bgPos: (Point)
*/
className: 'leaflet-div-icon',
html: false
},
createIcon: function (oldIcon) {
var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'),
options = this.options;
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';
}
this._setIconStyles(div, 'icon');
return div;
},
createShadow: function () {
return null;
}
});
L.divIcon = function (options) {
return new L.DivIcon(options);
};
/*
* L.Popup is used for displaying popups on the map.
*/
L.Map.mergeOptions({
closePopupOnClick: true
});
L.Popup = L.Class.extend({
includes: L.Mixin.Events,
options: {
minWidth: 50,
maxWidth: 300,
// maxHeight: null,
autoPan: true,
closeButton: true,
offset: [0, 7],
autoPanPadding: [5, 5],
// autoPanPaddingTopLeft: null,
// autoPanPaddingBottomRight: null,
keepInView: false,
className: '',
zoomAnimation: true
},
initialize: function (options, source) {
L.setOptions(this, options);
this._source = source;
this._animated = L.Browser.any3d && this.options.zoomAnimation;
this._isOpen = false;
},
onAdd: function (map) {
this._map = map;
if (!this._container) {
this._initLayout();
}
var animFade = map.options.fadeAnimation;
if (animFade) {
L.DomUtil.setOpacity(this._container, 0);
}
map._panes.popupPane.appendChild(this._container);
map.on(this._getEvents(), this);
this.update();
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});
}
},
addTo: function (map) {
map.addLayer(this);
return this;
},
openOn: function (map) {
map.openPopup(this);
return this;
},
onRemove: function (map) {
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._map = null;
this.fire('close');
map.fire('popupclose', {popup: this});
if (this._source) {
this._source.fire('popupclose', {popup: this});
}
},
getLatLng: function () {
return this._latlng;
},
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
if (this._map) {
this._updatePosition();
this._adjustPan();
}
return this;
},
getContent: function () {
return this._content;
},
setContent: function (content) {
this._content = content;
this.update();
return this;
},
update: function () {
if (!this._map) { return; }
this._container.style.visibility = 'hidden';
this._updateContent();
this._updateLayout();
this._updatePosition();
this._container.style.visibility = '';
this._adjustPan();
},
_getEvents: function () {
var events = {
viewreset: this._updatePosition
};
if (this._animated) {
events.zoomanim = this._zoomAnimation;
}
if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
events.preclick = this._close;
}
if (this.options.keepInView) {
events.moveend = this._adjustPan;
}
return events;
},
_close: function () {
if (this._map) {
this._map.closePopup(this);
}
},
_initLayout: function () {
var prefix = 'leaflet-popup',
containerClass = prefix + ' ' + this.options.className + ' leaflet-zoom-' +
(this._animated ? 'animated' : 'hide'),
container = this._container = L.DomUtil.create('div', containerClass),
closeButton;
if (this.options.closeButton) {
closeButton = this._closeButton =
L.DomUtil.create('a', prefix + '-close-button', container);
closeButton.href = '#close';
closeButton.innerHTML = '&#215;';
L.DomEvent.disableClickPropagation(closeButton);
L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this);
}
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.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);
},
_updateContent: function () {
if (!this._content) { return; }
if (typeof this._content === 'string') {
this._contentNode.innerHTML = this._content;
} else {
while (this._contentNode.hasChildNodes()) {
this._contentNode.removeChild(this._contentNode.firstChild);
}
this._contentNode.appendChild(this._content);
}
this.fire('contentupdate');
},
_updateLayout: function () {
var container = this._contentNode,
style = container.style;
style.width = '';
style.whiteSpace = 'nowrap';
var width = container.offsetWidth;
width = Math.min(width, this.options.maxWidth);
width = Math.max(width, this.options.minWidth);
style.width = (width + 1) + 'px';
style.whiteSpace = '';
style.height = '';
var height = container.offsetHeight,
maxHeight = this.options.maxHeight,
scrolledClass = 'leaflet-popup-scrolled';
if (maxHeight && height > maxHeight) {
style.height = maxHeight + 'px';
L.DomUtil.addClass(container, scrolledClass);
} else {
L.DomUtil.removeClass(container, scrolledClass);
}
this._containerWidth = this._container.offsetWidth;
},
_updatePosition: function () {
if (!this._map) { return; }
var pos = this._map.latLngToLayerPoint(this._latlng),
animated = this._animated,
offset = L.point(this.options.offset);
if (animated) {
L.DomUtil.setPosition(this._container, pos);
}
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 = this._containerBottom + 'px';
this._container.style.left = this._containerLeft + 'px';
},
_zoomAnimation: function (opt) {
var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center);
L.DomUtil.setPosition(this._container, pos);
},
_adjustPan: function () {
if (!this.options.autoPan) { return; }
var map = this._map,
containerHeight = this._container.offsetHeight,
containerWidth = this._containerWidth,
layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
if (this._animated) {
layerPos._add(L.DomUtil.getPosition(this._container));
}
var containerPos = map.layerPointToContainerPoint(layerPos),
padding = L.point(this.options.autoPanPadding),
paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding),
paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding),
size = map.getSize(),
dx = 0,
dy = 0;
if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
dx = containerPos.x + containerWidth - size.x + paddingBR.x;
}
if (containerPos.x - dx - paddingTL.x < 0) { // left
dx = containerPos.x - paddingTL.x;
}
if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
dy = containerPos.y + containerHeight - size.y + paddingBR.y;
}
if (containerPos.y - dy - paddingTL.y < 0) { // top
dy = containerPos.y - paddingTL.y;
}
if (dx || dy) {
map
.fire('autopanstart')
.panBy([dx, dy]);
}
},
_onCloseButtonClick: function (e) {
this._close();
L.DomEvent.stop(e);
}
});
L.popup = function (options, source) {
return new L.Popup(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)
.setLatLng(latlng)
.setContent(content);
}
popup._isOpen = true;
this._popup = popup;
return this.addLayer(popup);
},
closePopup: function (popup) {
if (!popup || popup === this._popup) {
popup = this._popup;
this._popup = null;
}
if (popup) {
this.removeLayer(popup);
popup._isOpen = false;
}
return this;
}
});
/*
* Popup extension to L.Marker, adding popup-related methods.
*/
L.Marker.include({
openPopup: function () {
if (this._popup && this._map && !this._map.hasLayer(this._popup)) {
this._popup.setLatLng(this._latlng);
this._map.openPopup(this._popup);
}
return this;
},
closePopup: function () {
if (this._popup) {
this._popup._close();
}
return this;
},
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);
}
});
/*
* L.LayerGroup is a class to combine several layers into one so that
* you can manipulate the group (e.g. add/remove it) as one layer.
*/
L.LayerGroup = L.Class.extend({
initialize: function (layers) {
this._layers = {};
var i, len;
if (layers) {
for (i = 0, len = layers.length; i < len; i++) {
this.addLayer(layers[i]);
}
}
},
addLayer: function (layer) {
var id = this.getLayerId(layer);
this._layers[id] = layer;
if (this._map) {
this._map.addLayer(layer);
}
return this;
},
removeLayer: function (layer) {
var id = layer in this._layers ? layer : this.getLayerId(layer);
if (this._map && this._layers[id]) {
this._map.removeLayer(this._layers[id]);
}
delete this._layers[id];
return this;
},
hasLayer: function (layer) {
if (!layer) { return false; }
return (layer in this._layers || this.getLayerId(layer) in this._layers);
},
clearLayers: function () {
this.eachLayer(this.removeLayer, this);
return this;
},
invoke: function (methodName) {
var args = Array.prototype.slice.call(arguments, 1),
i, layer;
for (i in this._layers) {
layer = this._layers[i];
if (layer[methodName]) {
layer[methodName].apply(layer, args);
}
}
return this;
},
onAdd: function (map) {
this._map = map;
this.eachLayer(map.addLayer, map);
},
onRemove: function (map) {
this.eachLayer(map.removeLayer, map);
this._map = null;
},
addTo: function (map) {
map.addLayer(this);
return this;
},
eachLayer: function (method, context) {
for (var i in this._layers) {
method.call(context, this._layers[i]);
}
return this;
},
getLayer: function (id) {
return this._layers[id];
},
getLayers: function () {
var layers = [];
for (var i in this._layers) {
layers.push(this._layers[i]);
}
return layers;
},
setZIndex: function (zIndex) {
return this.invoke('setZIndex', zIndex);
},
getLayerId: function (layer) {
return L.stamp(layer);
}
});
L.layerGroup = function (layers) {
return new L.LayerGroup(layers);
};
/*
* L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods
* shared between a group of interactive layers (like vectors or markers).
*/
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;
}
if ('on' in layer) {
layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this);
}
L.LayerGroup.prototype.addLayer.call(this, layer);
if (this._popupContent && layer.bindPopup) {
layer.bindPopup(this._popupContent, this._popupOptions);
}
return this.fire('layeradd', {layer: layer});
},
removeLayer: function (layer) {
if (!this.hasLayer(layer)) {
return this;
}
if (layer in this._layers) {
layer = this._layers[layer];
}
layer.off(L.FeatureGroup.EVENTS, this._propagateEvent, this);
L.LayerGroup.prototype.removeLayer.call(this, layer);
if (this._popupContent) {
this.invoke('unbindPopup');
}
return this.fire('layerremove', {layer: layer});
},
bindPopup: function (content, options) {
this._popupContent = content;
this._popupOptions = options;
return this.invoke('bindPopup', content, options);
},
openPopup: function (latlng) {
// open popup on the first layer
for (var id in this._layers) {
this._layers[id].openPopup(latlng);
break;
}
return this;
},
setStyle: function (style) {
return this.invoke('setStyle', style);
},
bringToFront: function () {
return this.invoke('bringToFront');
},
bringToBack: function () {
return this.invoke('bringToBack');
},
getBounds: function () {
var bounds = new L.LatLngBounds();
this.eachLayer(function (layer) {
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);
}
});
L.featureGroup = function (layers) {
return new L.FeatureGroup(layers);
};
/*
* L.Path is a base class for rendering vector paths on a map. Inherited by Polyline, Circle, etc.
*/
L.Path = L.Class.extend({
includes: [L.Mixin.Events],
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));
})()
},
options: {
stroke: true,
color: '#0033ff',
dashArray: null,
lineCap: null,
lineJoin: null,
weight: 5,
opacity: 0.5,
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();
}
this.projectLatlngs();
this._updatePath();
if (this._container) {
this._map._pathRoot.appendChild(this._container);
}
this.fire('add');
map.on({
'viewreset': this.projectLatlngs,
'moveend': this._updatePath
}, this);
},
addTo: function (map) {
map.addLayer(this);
return this;
},
onRemove: function (map) {
map._pathRoot.removeChild(this._container);
// 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;
if (L.Browser.vml) {
this._container = null;
this._stroke = null;
this._fill = null;
}
map.off({
'viewreset': this.projectLatlngs,
'moveend': this._updatePath
}, this);
},
projectLatlngs: function () {
// do all projection stuff here
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._container) {
this._updateStyle();
}
return this;
},
redraw: function () {
if (this._map) {
this.projectLatlngs();
this._updatePath();
}
return this;
}
});
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());
this._pathViewport = new L.Bounds(min, max);
}
});
/*
* Extends L.Path with SVG-specific rendering code.
*/
L.Path.SVG_NS = 'http://www.w3.org/2000/svg';
L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect);
L.Path = L.Path.extend({
statics: {
SVG: L.Browser.svg
},
bringToFront: function () {
var root = this._map._pathRoot,
path = this._container;
if (path && root.lastChild !== path) {
root.appendChild(path);
}
return this;
},
bringToBack: function () {
var root = this._map._pathRoot,
path = this._container,
first = root.firstChild;
if (path && first !== path) {
root.insertBefore(path, first);
}
return this;
},
getPathString: function () {
// form path string here
},
_createElement: function (name) {
return document.createElementNS(L.Path.SVG_NS, name);
},
_initElements: function () {
this._map._initPathRoot();
this._initPath();
this._initStyle();
},
_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);
},
_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();
},
_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');
}
},
_updatePath: function () {
var str = this.getPathString();
if (!str) {
// fix webkit empty string parsing bug
str = 'M0 0';
}
this._path.setAttribute('d', str);
},
// 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');
}
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);
}
}
},
_onMouseClick: function (e) {
if (this._map.dragging && this._map.dragging.moved()) { return; }
this._fireMouseEvent(e);
},
_fireMouseEvent: function (e) {
if (!this.hasEventListeners(e.type)) { return; }
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);
}
}
});
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();
}
},
_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;
},
_endPathZoom: function () {
this._pathZooming = false;
},
_updateSvgViewport: function () {
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;
}
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);
}
L.DomUtil.setPosition(root, min);
root.setAttribute('width', width);
root.setAttribute('height', height);
root.setAttribute('viewBox', [min.x, min.y, width, height].join(' '));
if (L.Browser.mobileWebkit) {
pane.appendChild(root);
}
}
});
/*
* Popup extension to L.Path (polylines, polygons, circles), adding popup-related methods.
*/
L.Path.include({
bindPopup: function (content, options) {
if (content instanceof L.Popup) {
this._popup = content;
} else {
if (!this._popup || options) {
this._popup = new L.Popup(options, this);
}
this._popup.setContent(content);
}
if (!this._popupHandlersAdded) {
this
.on('click', this._openPopup, this)
.on('remove', this.closePopup, this);
this._popupHandlersAdded = true;
}
return this;
},
unbindPopup: function () {
if (this._popup) {
this._popup = null;
this
.off('click', this._openPopup)
.off('remove', this.closePopup);
this._popupHandlersAdded = false;
}
return this;
},
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);
}
});
/*
* 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 = '<v:shape adj="1"/>';
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('<lvml:' + name + ' class="lvml">');
};
} 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();
}
});
/*
* 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);
}
});
/*
* 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);
}
};
/*
* 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);
};
/*
* 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;
};
/*
* 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);
};
/*
* 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);
};
}());
/*
* 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.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.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);
};
/*
* 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;
}
});
/*
* 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;
}
});
/*
* 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);
}
});
/*
* CircleMarker canvas specific drawing parts.
*/
L.CircleMarker.include(!L.Path.CANVAS ? {} : {
_updateStyle: function () {
L.Path.prototype._updateStyle.call(this);
}
});
/*
* L.GeoJSON turns any GeoJSON data into a Leaflet layer.
*/
L.GeoJSON = L.FeatureGroup.extend({
initialize: function (geojson, options) {
L.setOptions(this, options);
this._layers = {};
if (geojson) {
this.addData(geojson);
}
},
addData: function (geojson) {
var features = L.Util.isArray(geojson) ? geojson : geojson.features,
i, len, feature;
if (features) {
for (i = 0, len = features.length; i < len; i++) {
// 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(features[i]);
}
}
return this;
}
var options = this.options;
if (options.filter && !options.filter(geojson)) { return; }
var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng, options);
layer.feature = L.GeoJSON.asFeature(geojson);
layer.defaultOptions = layer.options;
this.resetStyle(layer);
if (options.onEachFeature) {
options.onEachFeature(geojson, layer);
}
return this.addLayer(layer);
},
resetStyle: function (layer) {
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) {
this.eachLayer(function (layer) {
this._setLayerStyle(layer, style);
}, this);
},
_setLayerStyle: function (layer, style) {
if (typeof style === 'function') {
style = style(layer.feature);
}
if (layer.setStyle) {
layer.setStyle(style);
}
}
});
L.extend(L.GeoJSON, {
geometryToLayer: function (geojson, pointToLayer, coordsToLatLng, vectorOptions) {
var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson,
coords = geometry.coordinates,
layers = [],
latlng, latlngs, i, len;
coordsToLatLng = coordsToLatLng || this.coordsToLatLng;
switch (geometry.type) {
case 'Point':
latlng = coordsToLatLng(coords);
return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng);
case 'MultiPoint':
for (i = 0, len = coords.length; i < len; i++) {
latlng = coordsToLatLng(coords[i]);
layers.push(pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng));
}
return new L.FeatureGroup(layers);
case 'LineString':
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, 2, coordsToLatLng);
return new L.MultiPolygon(latlngs, vectorOptions);
case 'GeometryCollection':
for (i = 0, len = geometry.geometries.length; i < len; i++) {
layers.push(this.geometryToLayer({
geometry: geometry.geometries[i],
type: 'Feature',
properties: geojson.properties
}, pointToLayer, coordsToLatLng, vectorOptions));
}
return new L.FeatureGroup(layers);
default:
throw new Error('Invalid GeoJSON object.');
}
},
coordsToLatLng: function (coords) { // (Array[, Boolean]) -> LatLng
return new L.LatLng(coords[1], coords[0], coords[2]);
},
coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { // (Array[, Number, Function]) -> Array
var latlng, i, len,
latlngs = [];
for (i = 0, len = coords.length; i < len; i++) {
latlng = levelsDeep ?
this.coordsToLatLngs(coords[i], levelsDeep - 1, coordsToLatLng) :
(coordsToLatLng || this.coordsToLatLng)(coords[i]);
latlngs.push(latlng);
}
return latlngs;
},
latLngToCoords: function (latlng) {
var coords = [latlng.lng, latlng.lat];
if (latlng.alt !== undefined) {
coords.push(latlng.alt);
}
return coords;
},
latLngsToCoords: function (latLngs) {
var coords = [];
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);
},
asFeature: function (geoJSON) {
if (geoJSON.type === 'Feature') {
return geoJSON;
}
return {
type: 'Feature',
properties: {},
geometry: geoJSON
};
}
});
var PointToGeoJSON = {
toGeoJSON: function () {
return L.GeoJSON.getFeature(this, {
type: 'Point',
coordinates: L.GeoJSON.latLngToCoords(this.getLatLng())
});
}
};
L.Marker.include(PointToGeoJSON);
L.Circle.include(PointToGeoJSON);
L.CircleMarker.include(PointToGeoJSON);
L.Polyline.include({
toGeoJSON: function () {
return L.GeoJSON.getFeature(this, {
type: 'LineString',
coordinates: L.GeoJSON.latLngsToCoords(this.getLatLngs())
});
}
});
L.Polygon.include({
toGeoJSON: function () {
var coords = [L.GeoJSON.latLngsToCoords(this.getLatLngs())],
i, len, hole;
coords[0].push(coords[0][0]);
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);
}
}
return L.GeoJSON.getFeature(this, {
type: 'Polygon',
coordinates: coords
});
}
});
(function () {
function multiToGeoJSON(type) {
return function () {
var coords = [];
this.eachLayer(function (layer) {
coords.push(layer.toGeoJSON().geometry.coordinates);
});
return L.GeoJSON.getFeature(this, {
type: type,
coordinates: coords
});
};
}
L.MultiPolyline.include({toGeoJSON: multiToGeoJSON('MultiLineString')});
L.MultiPolygon.include({toGeoJSON: multiToGeoJSON('MultiPolygon')});
L.LayerGroup.include({
toGeoJSON: function () {
var geometry = this.feature && this.feature.geometry,
jsons = [],
json;
if (geometry && geometry.type === 'MultiPoint') {
return multiToGeoJSON('MultiPoint').call(this);
}
var isGeometryCollection = geometry && geometry.type === 'GeometryCollection';
this.eachLayer(function (layer) {
if (layer.toGeoJSON) {
json = layer.toGeoJSON();
jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json));
}
});
if (isGeometryCollection) {
return L.GeoJSON.getFeature(this, {
geometries: jsons,
type: 'GeometryCollection'
});
}
return {
type: 'FeatureCollection',
features: jsons
};
}
});
}());
L.geoJson = function (geojson, options) {
return new L.GeoJSON(geojson, options);
};
/*
* L.DomEvent contains functions for working with DOM events.
*/
L.DomEvent = {
/* inspired by John Resig, Dean Edwards and YUI addEvent implementations */
addListener: function (obj, type, fn, context) { // (HTMLElement, String, Function[, Object])
var id = L.stamp(fn),
key = '_leaflet_' + type + id,
handler, originalHandler, newType;
if (obj[key]) { return this; }
handler = function (e) {
return fn.call(context || obj, e || L.DomEvent._getEvent());
};
if (L.Browser.pointer && type.indexOf('touch') === 0) {
return this.addPointerListener(obj, type, handler, id);
}
if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
this.addDoubleTapListener(obj, handler, id);
}
if ('addEventListener' in obj) {
if (type === 'mousewheel') {
obj.addEventListener('DOMMouseScroll', handler, false);
obj.addEventListener(type, handler, false);
} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
originalHandler = handler;
newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
handler = function (e) {
if (!L.DomEvent._checkMouse(obj, e)) { return; }
return originalHandler(e);
};
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 {
obj.addEventListener(type, handler, false);
}
} else if ('attachEvent' in obj) {
obj.attachEvent('on' + type, handler);
}
obj[key] = handler;
return this;
},
removeListener: function (obj, type, fn) { // (HTMLElement, String, Function)
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);
} else if ('removeEventListener' in obj) {
if (type === 'mousewheel') {
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, handler, false);
}
} else if ('detachEvent' in obj) {
obj.detachEvent('on' + type, handler);
}
obj[key] = null;
return this;
},
stopPropagation: function (e) {
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
L.DomEvent._skipped(e);
return this;
},
disableScrollPropagation: function (el) {
var stop = L.DomEvent.stopPropagation;
return L.DomEvent
.on(el, 'mousewheel', stop)
.on(el, 'MozMousePixelScroll', stop);
},
disableClickPropagation: function (el) {
var stop = L.DomEvent.stopPropagation;
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)
.on(el, 'dblclick', stop);
},
preventDefault: function (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
},
stop: function (e) {
return L.DomEvent
.preventDefault(e)
.stopPropagation(e);
},
getMousePosition: function (e, container) {
if (!container) {
return new L.Point(e.clientX, e.clientY);
}
var rect = container.getBoundingClientRect();
return new L.Point(
e.clientX - rect.left - container.clientLeft,
e.clientY - rect.top - container.clientTop);
},
getWheelDelta: function (e) {
var delta = 0;
if (e.wheelDelta) {
delta = e.wheelDelta / 120;
}
if (e.detail) {
delta = -e.detail / 3;
}
return delta;
},
_skipEvents: {},
_fakeStop: function (e) {
// fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e)
L.DomEvent._skipEvents[e.type] = true;
},
_skipped: function (e) {
var skipped = this._skipEvents[e.type];
// reset when checking, as it's only used in map container and propagates outside of the map
this._skipEvents[e.type] = false;
return skipped;
},
// check if element really left/entered the event target (for mouseenter/mouseleave)
_checkMouse: function (el, e) {
var related = e.relatedTarget;
if (!related) { return true; }
try {
while (related && (related !== el)) {
related = related.parentNode;
}
} catch (err) {
return false;
}
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),
elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick);
// are they closer together than 500ms yet more than 100ms?
// Android typically triggers them ~300ms apart while multiple listeners
// on the same event should be triggered far faster;
// or check if click is simulated on the element, and if it is, reject any non-simulated events
if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
L.DomEvent.stop(e);
return;
}
L.DomEvent._lastClick = timeStamp;
return handler(e);
}
};
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.Class.extend({
includes: L.Mixin.Events,
statics: {
START: L.Browser.touch ? ['touchstart', 'mousedown'] : ['mousedown'],
END: {
mousedown: 'mouseup',
touchstart: 'touchend',
pointerdown: 'touchend',
MSPointerDown: 'touchend'
},
MOVE: {
mousedown: 'mousemove',
touchstart: 'touchmove',
pointerdown: 'touchmove',
MSPointerDown: 'touchmove'
}
},
initialize: function (element, dragStartTarget) {
this._element = element;
this._dragStartTarget = dragStartTarget || element;
},
enable: function () {
if (this._enabled) { return; }
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;
},
disable: function () {
if (!this._enabled) { return; }
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;
},
_onDown: function (e) {
this._moved = false;
if (e.shiftKey || ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; }
L.DomEvent.stopPropagation(e);
if (L.Draggable._disabled) { return; }
L.DomUtil.disableImageDrag();
L.DomUtil.disableTextSelection();
if (this._moving) { return; }
var first = e.touches ? e.touches[0] : e;
this._startPoint = new L.Point(first.clientX, first.clientY);
this._startPos = this._newPos = L.DomUtil.getPosition(this._element);
L.DomEvent
.on(document, L.Draggable.MOVE[e.type], this._onMove, this)
.on(document, L.Draggable.END[e.type], this._onUp, this);
},
_onMove: function (e) {
if (e.touches && e.touches.length > 1) {
this._moved = true;
return;
}
var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e),
newPoint = new L.Point(first.clientX, first.clientY),
offset = newPoint.subtract(this._startPoint);
if (!offset.x && !offset.y) { return; }
if (L.Browser.touch && Math.abs(offset.x) + Math.abs(offset.y) < 3) { return; }
L.DomEvent.preventDefault(e);
if (!this._moved) {
this.fire('dragstart');
this._moved = true;
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');
}
this._newPos = this._startPos.add(offset);
this._moving = true;
L.Util.cancelAnimFrame(this._animRequest);
this._animRequest = L.Util.requestAnimFrame(this._updatePosition, this, true, this._dragStartTarget);
},
_updatePosition: function () {
this.fire('predrag');
L.DomUtil.setPosition(this._element, this._newPos);
this.fire('drag');
},
_onUp: function () {
L.DomUtil.removeClass(document.body, 'leaflet-dragging');
if (this._lastTarget) {
L.DomUtil.removeClass(this._lastTarget, 'leaflet-drag-target');
this._lastTarget = null;
}
for (var i in L.Draggable.MOVE) {
L.DomEvent
.off(document, L.Draggable.MOVE[i], this._onMove)
.off(document, L.Draggable.END[i], this._onUp);
}
L.DomUtil.enableImageDrag();
L.DomUtil.enableTextSelection();
if (this._moved && this._moving) {
// ensure drag is not fired after dragend
L.Util.cancelAnimFrame(this._animRequest);
this.fire('dragend', {
distance: this._newPos.distanceTo(this._startPos)
});
}
this._moving = false;
}
});
/*
L.Handler is a base class for handler classes that are used internally to inject
interaction features like dragging to classes like Map and Marker.
*/
L.Handler = L.Class.extend({
initialize: function (map) {
this._map = map;
},
enable: function () {
if (this._enabled) { return; }
this._enabled = true;
this.addHooks();
},
disable: function () {
if (!this._enabled) { return; }
this._enabled = false;
this.removeHooks();
},
enabled: function () {
return !!this._enabled;
}
});
/*
* L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
*/
L.Map.mergeOptions({
dragging: true,
inertia: !L.Browser.android23,
inertiaDeceleration: 3400, // px/s^2
inertiaMaxSpeed: Infinity, // px/s
inertiaThreshold: L.Browser.touch ? 32 : 18, // ms
easeLinearity: 0.25,
// TODO refactor, move to CRS
worldCopyJump: false
});
L.Map.Drag = L.Handler.extend({
addHooks: function () {
if (!this._draggable) {
var map = this._map;
this._draggable = new L.Draggable(map._mapPane, map._container);
this._draggable.on({
'dragstart': this._onDragStart,
'drag': this._onDrag,
'dragend': this._onDragEnd
}, this);
if (map.options.worldCopyJump) {
this._draggable.on('predrag', this._onPreDrag, this);
map.on('viewreset', this._onViewReset, this);
map.whenReady(this._onViewReset, this);
}
}
this._draggable.enable();
},
removeHooks: function () {
this._draggable.disable();
},
moved: function () {
return this._draggable && this._draggable._moved;
},
_onDragStart: function () {
var map = this._map;
if (map._panAnim) {
map._panAnim.stop();
}
map
.fire('movestart')
.fire('dragstart');
if (map.options.inertia) {
this._positions = [];
this._times = [];
}
},
_onDrag: function () {
if (this._map.options.inertia) {
var time = this._lastTime = +new Date(),
pos = this._lastPos = this._draggable._newPos;
this._positions.push(pos);
this._times.push(time);
if (time - this._times[0] > 200) {
this._positions.shift();
this._times.shift();
}
}
this._map
.fire('move')
.fire('drag');
},
_onViewReset: function () {
// 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.project([0, 180]).x;
},
_onPreDrag: function () {
// TODO refactor to be able to adjust map pane position after zoom
var worldWidth = this._worldWidth,
halfWidth = Math.round(worldWidth / 2),
dx = this._initialWorldOffset,
x = this._draggable._newPos.x,
newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
this._draggable._newPos.x = newX;
},
_onDragEnd: function (e) {
var map = this._map,
options = map.options,
delay = +new Date() - this._lastTime,
noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0];
map.fire('dragend', e);
if (noInertia) {
map.fire('moveend');
} else {
var direction = this._lastPos.subtract(this._positions[0]),
duration = (this._lastTime + delay - this._times[0]) / 1000,
ease = options.easeLinearity,
speedVector = direction.multiplyBy(ease / duration),
speed = speedVector.distanceTo([0, 0]),
limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
if (!offset.x || !offset.y) {
map.fire('moveend');
} else {
offset = map._limitOffset(offset, map.options.maxBounds);
L.Util.requestAnimFrame(function () {
map.panBy(offset, {
duration: decelerationDuration,
easeLinearity: ease,
noMoveStart: true
});
});
}
}
}
});
L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);
/*
* L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default.
*/
L.Map.mergeOptions({
doubleClickZoom: true
});
L.Map.DoubleClickZoom = L.Handler.extend({
addHooks: function () {
this._map.on('dblclick', this._onDoubleClick, this);
},
removeHooks: function () {
this._map.off('dblclick', this._onDoubleClick, this);
},
_onDoubleClick: function (e) {
var map = this._map,
zoom = map.getZoom() + (e.originalEvent.shiftKey ? -1 : 1);
if (map.options.doubleClickZoom === 'center') {
map.setZoom(zoom);
} else {
map.setZoomAround(e.containerPoint, zoom);
}
}
});
L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom);
/*
* L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
*/
L.Map.mergeOptions({
scrollWheelZoom: true
});
L.Map.ScrollWheelZoom = L.Handler.extend({
addHooks: function () {
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);
L.DomEvent.off(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault);
},
_onWheelScroll: function (e) {
var delta = L.DomEvent.getWheelDelta(e);
this._delta += delta;
this._lastMousePos = this._map.mouseEventToContainerPoint(e);
if (!this._startTime) {
this._startTime = +new Date();
}
var left = Math.max(40 - (+new Date() - this._startTime), 0);
clearTimeout(this._timer);
this._timer = setTimeout(L.bind(this._performZoom, this), left);
L.DomEvent.preventDefault(e);
L.DomEvent.stopPropagation(e);
},
_performZoom: function () {
var map = this._map,
delta = this._delta,
zoom = map.getZoom();
delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta);
delta = Math.max(Math.min(delta, 4), -4);
delta = map._limitZoom(zoom + delta) - zoom;
this._delta = 0;
this._startTime = null;
if (!delta) { return; }
if (map.options.scrollWheelZoom === 'center') {
map.setZoom(zoom + delta);
} else {
map.setZoomAround(this._lastMousePos, zoom + delta);
}
}
});
L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);
/*
* Extends the event handling code with double tap support for mobile browsers.
*/
L.extend(L.DomEvent, {
_touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
_touchend: L.Browser.msPointer ? 'MSPointerUp' : L.Browser.pointer ? 'pointerup' : 'touchend',
// inspired by Zepto touch code by Thomas Fuchs
addDoubleTapListener: function (obj, handler, id) {
var last,
doubleTap = false,
delay = 250,
touch,
pre = '_leaflet_',
touchstart = this._touchstart,
touchend = this._touchend,
trackedTouches = [];
function onTouchStart(e) {
var count;
if (L.Browser.pointer) {
trackedTouches.push(e.pointerId);
count = trackedTouches.length;
} else {
count = e.touches.length;
}
if (count > 1) {
return;
}
var now = Date.now(),
delta = now - (last || now);
touch = e.touches ? e.touches[0] : e;
doubleTap = (delta > 0 && delta <= delay);
last = now;
}
function onTouchEnd(e) {
if (L.Browser.pointer) {
var idx = trackedTouches.indexOf(e.pointerId);
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;
// jshint forin:false
for (var i in touch) {
prop = touch[i];
if (typeof prop === 'function') {
newTouch[i] = prop.bind(touch);
} else {
newTouch[i] = prop;
}
}
touch = newTouch;
}
touch.type = 'dblclick';
handler(touch);
last = null;
}
}
obj[pre + touchstart + id] = onTouchStart;
obj[pre + touchend + id] = onTouchEnd;
// on pointer we need to listen on the document, otherwise a drag starting on the map and moving off screen
// will not come through to us, so we will lose track of how many touches are ongoing
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);
}
return this;
},
removeDoubleTapListener: function (obj, 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);
if (L.Browser.pointer) {
document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id],
false);
}
return this;
}
});
/*
* Extends L.DomEvent to provide touch support for Internet Explorer and Windows-based devices.
*/
L.extend(L.DomEvent, {
//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: [],
_pointerDocumentListener: false,
// Provides a touch events wrapper for (ms)pointer events.
// 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) {
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';
}
},
addPointerListenerStart: function (obj, type, handler, id) {
var pre = '_leaflet_',
pointers = this._pointers;
var cb = function (e) {
L.DomEvent.preventDefault(e);
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];
handler(e);
};
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);
this._pointerDocumentListener = true;
}
return this;
},
addPointerListenerMove: function (obj, type, handler, id) {
var pre = '_leaflet_',
touches = this._pointers;
function cb(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; }
for (var i = 0; i < touches.length; i++) {
if (touches[i].pointerId === e.pointerId) {
touches[i] = e;
break;
}
}
e.touches = touches.slice();
e.changedTouches = [e];
handler(e);
}
obj[pre + 'touchmove' + id] = cb;
obj.addEventListener(this.POINTER_MOVE, cb, false);
return this;
},
addPointerListenerEnd: function (obj, type, handler, id) {
var pre = '_leaflet_',
touches = this._pointers;
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);
return 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;
}
return this;
}
});
/*
* L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers.
*/
L.Map.mergeOptions({
touchZoom: L.Browser.touch && !L.Browser.android23,
bounceAtZoomLimits: true
});
L.Map.TouchZoom = L.Handler.extend({
addHooks: function () {
L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
},
removeHooks: function () {
L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
},
_onTouchStart: function (e) {
var map = this._map;
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
var p1 = map.mouseEventToLayerPoint(e.touches[0]),
p2 = map.mouseEventToLayerPoint(e.touches[1]),
viewCenter = map._getCenterLayerPoint();
this._startCenter = p1.add(p2)._divideBy(2);
this._startDist = p1.distanceTo(p2);
this._moved = false;
this._zooming = true;
this._centerOffset = viewCenter.subtract(this._startCenter);
if (map._panAnim) {
map._panAnim.stop();
}
L.DomEvent
.on(document, 'touchmove', this._onTouchMove, this)
.on(document, 'touchend', this._onTouchEnd, this);
L.DomEvent.preventDefault(e);
},
_onTouchMove: function (e) {
var map = this._map;
if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; }
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) {
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');
this._moved = true;
}
L.Util.cancelAnimFrame(this._animRequest);
this._animRequest = L.Util.requestAnimFrame(
this._updateOnMove, this, true, this._map._container);
L.DomEvent.preventDefault(e);
},
_updateOnMove: function () {
var map = this._map,
origin = this._getScaleOrigin(),
center = map.layerPointToLatLng(origin),
zoom = map.getScaleZoom(this._scale);
map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, false, true);
},
_onTouchEnd: function () {
if (!this._moved || !this._zooming) {
this._zooming = false;
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 origin = this._getScaleOrigin(),
center = map.layerPointToLatLng(origin),
oldZoom = map.getZoom(),
floatZoomDelta = map.getScaleZoom(this._scale) - oldZoom,
roundZoomDelta = (floatZoomDelta > 0 ?
Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
zoom = map._limitZoom(oldZoom + roundZoomDelta),
scale = map.getZoomScale(zoom) / this._scale;
map._animateZoom(center, zoom, origin, scale);
},
_getScaleOrigin: function () {
var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale);
return this._startCenter.add(centerOffset);
}
});
L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
/*
* L.Map.Tap is used to enable mobile hacks like quick taps and long hold.
*/
L.Map.mergeOptions({
tap: true,
tapTolerance: 15
});
L.Map.Tap = L.Handler.extend({
addHooks: function () {
L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
},
removeHooks: function () {
L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
},
_onDown: function (e) {
if (!e.touches) { return; }
L.DomEvent.preventDefault(e);
this._fireClick = true;
// don't simulate click or track longpress if more than 1 touch
if (e.touches.length > 1) {
this._fireClick = false;
clearTimeout(this._holdTimeout);
return;
}
var first = e.touches[0],
el = first.target;
this._startPos = this._newPos = new L.Point(first.clientX, first.clientY);
// if touching a link, highlight it
if (el.tagName && el.tagName.toLowerCase() === 'a') {
L.DomUtil.addClass(el, 'leaflet-active');
}
// simulate long hold but setting a timeout
this._holdTimeout = setTimeout(L.bind(function () {
if (this._isTapValid()) {
this._fireClick = false;
this._onUp();
this._simulateEvent('contextmenu', first);
}
}, this), 1000);
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, this)
.off(document, 'touchend', this._onUp, this);
if (this._fireClick && e && e.changedTouches) {
var first = e.changedTouches[0],
el = first.target;
if (el && el.tagName && el.tagName.toLowerCase() === 'a') {
L.DomUtil.removeClass(el, 'leaflet-active');
}
// simulate click if the touch didn't move too much
if (this._isTapValid()) {
this._simulateEvent('click', first);
}
}
},
_isTapValid: function () {
return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
},
_onMove: function (e) {
var first = e.touches[0];
this._newPos = new L.Point(first.clientX, first.clientY);
},
_simulateEvent: function (type, e) {
var simulatedEvent = document.createEvent('MouseEvents');
simulatedEvent._simulated = true;
e.target._simulatedClick = true;
simulatedEvent.initMouseEvent(
type, true, true, window, 1,
e.screenX, e.screenY,
e.clientX, e.clientY,
false, false, false, false, 0, null);
e.target.dispatchEvent(simulatedEvent);
}
});
if (L.Browser.touch && !L.Browser.pointer) {
L.Map.addInitHook('addHandler', 'tap', L.Map.Tap);
}
/*
* L.Handler.ShiftDragZoom is used to add shift-drag zoom interaction to the map
* (zoom to a selected bounding box), enabled by default.
*/
L.Map.mergeOptions({
boxZoom: true
});
L.Map.BoxZoom = L.Handler.extend({
initialize: function (map) {
this._map = map;
this._container = map._container;
this._pane = map._panes.overlayPane;
this._moved = false;
},
addHooks: function () {
L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
},
removeHooks: function () {
L.DomEvent.off(this._container, 'mousedown', this._onMouseDown);
this._moved = false;
},
moved: function () {
return this._moved;
},
_onMouseDown: function (e) {
this._moved = false;
if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
L.DomUtil.disableTextSelection();
L.DomUtil.disableImageDrag();
this._startLayerPoint = this._map.mouseEventToLayerPoint(e);
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._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');
}
var startPoint = this._startLayerPoint,
box = this._box,
layerPoint = this._map.mouseEventToLayerPoint(e),
offset = layerPoint.subtract(startPoint),
newPos = new L.Point(
Math.min(layerPoint.x, startPoint.x),
Math.min(layerPoint.y, startPoint.y));
L.DomUtil.setPosition(box, newPos);
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) {
this._pane.removeChild(this._box);
this._container.style.cursor = '';
}
L.DomUtil.enableTextSelection();
L.DomUtil.enableImageDrag();
L.DomEvent
.off(document, 'mousemove', this._onMouseMove)
.off(document, 'mouseup', this._onMouseUp)
.off(document, 'keydown', this._onKeyDown);
},
_onMouseUp: function (e) {
this._finish();
var map = this._map,
layerPoint = map.mouseEventToLayerPoint(e);
if (this._startLayerPoint.equals(layerPoint)) { return; }
var bounds = new L.LatLngBounds(
map.layerPointToLatLng(this._startLayerPoint),
map.layerPointToLatLng(layerPoint));
map.fitBounds(bounds);
map.fire('boxzoomend', {
boxZoomBounds: bounds
});
},
_onKeyDown: function (e) {
if (e.keyCode === 27) {
this._finish();
}
}
});
L.Map.addInitHook('addHandler', 'boxZoom', L.Map.BoxZoom);
/*
* L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
*/
L.Map.mergeOptions({
keyboard: true,
keyboardPanOffset: 80,
keyboardZoomOffset: 1
});
L.Map.Keyboard = L.Handler.extend({
keyCodes: {
left: [37],
right: [39],
down: [40],
up: [38],
zoomIn: [187, 107, 61, 171],
zoomOut: [189, 109, 173]
},
initialize: function (map) {
this._map = map;
this._setPanOffset(map.options.keyboardPanOffset);
this._setZoomOffset(map.options.keyboardZoomOffset);
},
addHooks: function () {
var container = this._map._container;
// make the container focusable by tabbing
if (container.tabIndex === -1) {
container.tabIndex = '0';
}
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, this)
.on('blur', this._removeHooks, this);
},
removeHooks: function () {
this._removeHooks();
var container = this._map._container;
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 () {
if (this._focused) { return; }
var body = document.body,
docEl = document.documentElement,
top = body.scrollTop || docEl.scrollTop,
left = body.scrollLeft || docEl.scrollLeft;
this._map._container.focus();
window.scrollTo(left, top);
},
_onFocus: function () {
this._focused = true;
this._map.fire('focus');
},
_onBlur: function () {
this._focused = false;
this._map.fire('blur');
},
_setPanOffset: function (pan) {
var keys = this._panKeys = {},
codes = this.keyCodes,
i, len;
for (i = 0, len = codes.left.length; i < len; i++) {
keys[codes.left[i]] = [-1 * pan, 0];
}
for (i = 0, len = codes.right.length; i < len; i++) {
keys[codes.right[i]] = [pan, 0];
}
for (i = 0, len = codes.down.length; i < len; i++) {
keys[codes.down[i]] = [0, pan];
}
for (i = 0, len = codes.up.length; i < len; i++) {
keys[codes.up[i]] = [0, -1 * pan];
}
},
_setZoomOffset: function (zoom) {
var keys = this._zoomKeys = {},
codes = this.keyCodes,
i, len;
for (i = 0, len = codes.zoomIn.length; i < len; i++) {
keys[codes.zoomIn[i]] = zoom;
}
for (i = 0, len = codes.zoomOut.length; i < len; i++) {
keys[codes.zoomOut[i]] = -zoom;
}
},
_addHooks: function () {
L.DomEvent.on(document, 'keydown', this._onKeyDown, this);
},
_removeHooks: function () {
L.DomEvent.off(document, 'keydown', this._onKeyDown, this);
},
_onKeyDown: function (e) {
var key = e.keyCode,
map = this._map;
if (key in this._panKeys) {
if (map._panAnim && map._panAnim._inProgress) { return; }
map.panBy(this._panKeys[key]);
if (map.options.maxBounds) {
map.panInsideBounds(map.options.maxBounds);
}
} else if (key in this._zoomKeys) {
map.setZoom(map.getZoom() + this._zoomKeys[key]);
} else {
return;
}
L.DomEvent.stop(e);
}
});
L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard);
/*
* L.Handler.MarkerDrag is used internally by L.Marker to make the markers draggable.
*/
L.Handler.MarkerDrag = L.Handler.extend({
initialize: function (marker) {
this._marker = marker;
},
addHooks: function () {
var icon = this._marker._icon;
if (!this._draggable) {
this._draggable = new L.Draggable(icon, icon);
}
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, this)
.off('drag', this._onDrag, this)
.off('dragend', this._onDragEnd, this);
this._draggable.disable();
L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable');
},
moved: function () {
return this._draggable && this._draggable._moved;
},
_onDragStart: function () {
this._marker
.closePopup()
.fire('movestart')
.fire('dragstart');
},
_onDrag: function () {
var marker = this._marker,
shadow = marker._shadow,
iconPos = L.DomUtil.getPosition(marker._icon),
latlng = marker._map.layerPointToLatLng(iconPos);
// update shadow position
if (shadow) {
L.DomUtil.setPosition(shadow, iconPos);
}
marker._latlng = latlng;
marker
.fire('move', {latlng: latlng})
.fire('drag');
},
_onDragEnd: function (e) {
this._marker
.fire('moveend')
.fire('dragend', e);
}
});
/*
* L.Control is a base class for implementing map controls. Handles positioning.
* All other controls extend from this class.
*/
L.Control = L.Class.extend({
options: {
position: 'topright'
},
initialize: function (options) {
L.setOptions(this, options);
},
getPosition: function () {
return this.options.position;
},
setPosition: function (position) {
var map = this._map;
if (map) {
map.removeControl(this);
}
this.options.position = position;
if (map) {
map.addControl(this);
}
return this;
},
getContainer: function () {
return this._container;
},
addTo: function (map) {
this._map = map;
var container = this._container = this.onAdd(map),
pos = this.getPosition(),
corner = map._controlCorners[pos];
L.DomUtil.addClass(container, 'leaflet-control');
if (pos.indexOf('bottom') !== -1) {
corner.insertBefore(container, corner.firstChild);
} else {
corner.appendChild(container);
}
return this;
},
removeFrom: function (map) {
var pos = this.getPosition(),
corner = map._controlCorners[pos];
corner.removeChild(this._container);
this._map = null;
if (this.onRemove) {
this.onRemove(map);
}
return this;
},
_refocusOnMap: function () {
if (this._map) {
this._map.getContainer().focus();
}
}
});
L.control = function (options) {
return new L.Control(options);
};
// adds control-related methods to L.Map
L.Map.include({
addControl: function (control) {
control.addTo(this);
return this;
},
removeControl: function (control) {
control.removeFrom(this);
return this;
},
_initControlPos: function () {
var corners = this._controlCorners = {},
l = 'leaflet-',
container = this._controlContainer =
L.DomUtil.create('div', l + 'control-container', this._container);
function createCorner(vSide, hSide) {
var className = l + vSide + ' ' + l + hSide;
corners[vSide + hSide] = L.DomUtil.create('div', className, container);
}
createCorner('top', 'left');
createCorner('top', 'right');
createCorner('bottom', 'left');
createCorner('bottom', 'right');
},
_clearControlPos: function () {
this._container.removeChild(this._controlContainer);
}
});
/*
* L.Control.Zoom is used for the default zoom buttons on the map.
*/
L.Control.Zoom = L.Control.extend({
options: {
position: 'topleft',
zoomInText: '+',
zoomInTitle: 'Zoom in',
zoomOutText: '-',
zoomOutTitle: 'Zoom out'
},
onAdd: function (map) {
var zoomName = 'leaflet-control-zoom',
container = L.DomUtil.create('div', zoomName + ' leaflet-bar');
this._map = map;
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);
return container;
},
onRemove: function (map) {
map.off('zoomend zoomlevelschange', this._updateDisabled, this);
},
_zoomIn: function (e) {
this._map.zoomIn(e.shiftKey ? 3 : 1);
},
_zoomOut: function (e) {
this._map.zoomOut(e.shiftKey ? 3 : 1);
},
_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, '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;
},
_updateDisabled: function () {
var map = this._map,
className = 'leaflet-disabled';
L.DomUtil.removeClass(this._zoomInButton, className);
L.DomUtil.removeClass(this._zoomOutButton, className);
if (map._zoom === map.getMinZoom()) {
L.DomUtil.addClass(this._zoomOutButton, className);
}
if (map._zoom === map.getMaxZoom()) {
L.DomUtil.addClass(this._zoomInButton, className);
}
}
});
L.Map.mergeOptions({
zoomControl: true
});
L.Map.addInitHook(function () {
if (this.options.zoomControl) {
this.zoomControl = new L.Control.Zoom();
this.addControl(this.zoomControl);
}
});
L.control.zoom = function (options) {
return new L.Control.Zoom(options);
};
/*
* L.Control.Attribution is used for displaying attribution on the map (added by default).
*/
L.Control.Attribution = L.Control.extend({
options: {
position: 'bottomright',
prefix: '<a href="http://leafletjs.com" title="A JS library for interactive maps">Leaflet</a>'
},
initialize: function (options) {
L.setOptions(this, options);
this._attributions = {};
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
L.DomEvent.disableClickPropagation(this._container);
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();
return this;
},
addAttribution: function (text) {
if (!text) { return; }
if (!this._attributions[text]) {
this._attributions[text] = 0;
}
this._attributions[text]++;
this._update();
return this;
},
removeAttribution: function (text) {
if (!text) { return; }
if (this._attributions[text]) {
this._attributions[text]--;
this._update();
}
return this;
},
_update: function () {
if (!this._map) { return; }
var attribs = [];
for (var i in this._attributions) {
if (this._attributions[i]) {
attribs.push(i);
}
}
var prefixAndAttribs = [];
if (this.options.prefix) {
prefixAndAttribs.push(this.options.prefix);
}
if (attribs.length) {
prefixAndAttribs.push(attribs.join(', '));
}
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());
}
}
});
L.Map.mergeOptions({
attributionControl: true
});
L.Map.addInitHook(function () {
if (this.options.attributionControl) {
this.attributionControl = (new L.Control.Attribution()).addTo(this);
}
});
L.control.attribution = function (options) {
return new L.Control.Attribution(options);
};
/*
* L.Control.Scale is used for displaying metric/imperial scale on the map.
*/
L.Control.Scale = L.Control.extend({
options: {
position: 'bottomleft',
maxWidth: 100,
metric: true,
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, container);
map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
map.whenReady(this._update, this);
return container;
},
onRemove: function (map) {
map.off(this.options.updateWhenIdle ? 'moveend' : 'move', this._update, this);
},
_addScales: function (options, className, container) {
if (options.metric) {
this._mScale = L.DomUtil.create('div', className + '-line', container);
}
if (options.imperial) {
this._iScale = L.DomUtil.create('div', className + '-line', container);
}
},
_update: function () {
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,
size = this._map.getSize(),
options = this.options,
maxMeters = 0;
if (size.x > 0) {
maxMeters = dist * (options.maxWidth / size.x);
}
this._updateScales(options, maxMeters);
},
_updateScales: function (options, maxMeters) {
if (options.metric && maxMeters) {
this._updateMetric(maxMeters);
}
if (options.imperial && maxMeters) {
this._updateImperial(maxMeters);
}
},
_updateMetric: function (maxMeters) {
var meters = this._getRoundNum(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);
scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px';
scale.innerHTML = miles + ' mi';
} else {
feet = this._getRoundNum(maxFeet);
scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px';
scale.innerHTML = feet + ' ft';
}
},
_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;
return pow10 * d;
}
});
L.control.scale = function (options) {
return new L.Control.Scale(options);
};
/*
* L.Control.Layers is a control to allow users to switch between different layers on the map.
*/
L.Control.Layers = L.Control.extend({
options: {
collapsed: true,
position: 'topright',
autoZIndex: true
},
initialize: function (baseLayers, overlays, options) {
L.setOptions(this, options);
this._layers = {};
this._lastZIndex = 0;
this._handlingClick = false;
for (var i in baseLayers) {
this._addLayer(baseLayers[i], i);
}
for (i in overlays) {
this._addLayer(overlays[i], i, true);
}
},
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);
this._update();
return this;
},
addOverlay: function (layer, name) {
this._addLayer(layer, name, true);
this._update();
return this;
},
removeLayer: function (layer) {
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 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) {
L.DomEvent
.disableClickPropagation(container)
.disableScrollPropagation(container);
} else {
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
}
var form = this._form = L.DomUtil.create('form', className + '-list');
if (this.options.collapsed) {
if (!L.Browser.android) {
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';
if (L.Browser.touch) {
L.DomEvent
.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._expand, this);
}
else {
L.DomEvent.on(link, 'focus', this._expand, this);
}
//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);
this._map.on('click', this._collapse, this);
// TODO keyboard accessibility
} else {
this._expand();
}
this._baseLayersList = L.DomUtil.create('div', className + '-base', form);
this._separator = L.DomUtil.create('div', className + '-separator', form);
this._overlaysList = L.DomUtil.create('div', className + '-overlays', form);
container.appendChild(form);
},
_addLayer: function (layer, name, overlay) {
var id = L.stamp(layer);
this._layers[id] = {
layer: layer,
name: name,
overlay: overlay
};
if (this.options.autoZIndex && layer.setZIndex) {
this._lastZIndex++;
layer.setZIndex(this._lastZIndex);
}
},
_update: function () {
if (!this._container) {
return;
}
this._baseLayersList.innerHTML = '';
this._overlaysList.innerHTML = '';
var baseLayersPresent = false,
overlaysPresent = false,
i, obj;
for (i in this._layers) {
obj = this._layers[i];
this._addItem(obj);
overlaysPresent = overlaysPresent || obj.overlay;
baseLayersPresent = baseLayersPresent || !obj.overlay;
}
this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none';
},
_onLayerChange: function (e) {
var obj = this._layers[L.stamp(e.layer)];
if (!obj) { return; }
if (!this._handlingClick) {
this._update();
}
var type = obj.overlay ?
(e.type === 'layeradd' ? 'overlayadd' : 'overlayremove') :
(e.type === 'layeradd' ? 'baselayerchange' : null);
if (type) {
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 = '<input type="radio" class="leaflet-control-layers-selector" name="' + name + '"';
if (checked) {
radioHtml += ' checked="checked"';
}
radioHtml += '/>';
var radioFragment = document.createElement('div');
radioFragment.innerHTML = radioHtml;
return radioFragment.firstChild;
},
_addItem: function (obj) {
var label = document.createElement('label'),
input,
checked = this._map.hasLayer(obj.layer);
if (obj.overlay) {
input = document.createElement('input');
input.type = 'checkbox';
input.className = 'leaflet-control-layers-selector';
input.defaultChecked = checked;
} else {
input = this._createRadioElement('leaflet-base-layers', checked);
}
input.layerId = L.stamp(obj.layer);
L.DomEvent.on(input, 'click', this._onInputClick, this);
var name = document.createElement('span');
name.innerHTML = ' ' + obj.name;
label.appendChild(input);
label.appendChild(name);
var container = obj.overlay ? this._overlaysList : this._baseLayersList;
container.appendChild(label);
return label;
},
_onInputClick: function () {
var i, input, obj,
inputs = this._form.getElementsByTagName('input'),
inputsLen = inputs.length;
this._handlingClick = true;
for (i = 0; i < inputsLen; i++) {
input = inputs[i];
obj = this._layers[input.layerId];
if (input.checked && !this._map.hasLayer(obj.layer)) {
this._map.addLayer(obj.layer);
} else if (!input.checked && this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
}
}
this._handlingClick = false;
this._refocusOnMap();
},
_expand: function () {
L.DomUtil.addClass(this._container, 'leaflet-control-layers-expanded');
},
_collapse: function () {
this._container.className = this._container.className.replace(' leaflet-control-layers-expanded', '');
}
});
L.control.layers = function (baseLayers, overlays, options) {
return new L.Control.Layers(baseLayers, overlays, options);
};
/*
* L.PosAnimation is used by Leaflet internally for pan animations.
*/
L.PosAnimation = L.Class.extend({
includes: L.Mixin.Events,
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
this.stop();
this._el = el;
this._inProgress = true;
this._newPos = newPos;
this.fire('start');
el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) +
's cubic-bezier(0,0,' + (easeLinearity || 0.5) + ',1)';
L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
L.DomUtil.setPosition(el, newPos);
// toggle reflow, Chrome flickers for some reason if you don't do this
L.Util.falseFn(el.offsetWidth);
// there's no native way to track value updates of transitioned properties, so we imitate this
this._stepTimer = setInterval(L.bind(this._onStep, this), 50);
},
stop: function () {
if (!this._inProgress) { return; }
// if we just removed the transition property, the element would jump to its final position,
// so we need to make it stay at the current position
L.DomUtil.setPosition(this._el, this._getPos());
this._onTransitionEnd();
L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation
},
_onStep: function () {
var stepPos = this._getPos();
if (!stepPos) {
this._onTransitionEnd();
return;
}
// jshint camelcase: false
// make L.DomUtil.getPosition return intermediate position value during animation
this._el._leaflet_pos = stepPos;
this.fire('step');
},
// you can't easily get intermediate values of properties animated with CSS3 Transitions,
// we need to parse computed style (in case of transform it returns matrix string)
_transformRe: /([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,
_getPos: function () {
var left, top, matches,
el = this._el,
style = window.getComputedStyle(el);
if (L.Browser.any3d) {
matches = style[L.DomUtil.TRANSFORM].match(this._transformRe);
if (!matches) { return; }
left = parseFloat(matches[1]);
top = parseFloat(matches[2]);
} else {
left = parseFloat(style.left);
top = parseFloat(style.top);
}
return new L.Point(left, top, true);
},
_onTransitionEnd: function () {
L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
if (!this._inProgress) { return; }
this._inProgress = false;
this._el.style[L.DomUtil.TRANSITION] = '';
// jshint camelcase: false
// make sure L.DomUtil.getPosition returns the final position value after animation
this._el._leaflet_pos = this._newPos;
clearInterval(this._stepTimer);
this.fire('step').fire('end');
}
});
/*
* Extends L.Map to handle panning animations.
*/
L.Map.include({
setView: function (center, zoom, options) {
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
center = this._limitCenter(L.latLng(center), zoom, this.options.maxBounds);
options = options || {};
if (this._panAnim) {
this._panAnim.stop();
}
if (this._loaded && !options.reset && options !== true) {
if (options.animate !== undefined) {
options.zoom = L.extend({animate: options.animate}, options.zoom);
options.pan = L.extend({animate: options.animate}, options.pan);
}
// try animating pan or zoom
var animated = (this._zoom !== zoom) ?
this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) :
this._tryAnimatedPan(center, options.pan);
if (animated) {
// prevent resize handler call, the view will refresh after animation anyway
clearTimeout(this._sizeTimer);
return this;
}
}
// animation didn't start, just reset the map view
this._resetView(center, zoom);
return this;
},
panBy: function (offset, options) {
offset = L.point(offset).round();
options = options || {};
if (!offset.x && !offset.y) {
return this;
}
if (!this._panAnim) {
this._panAnim = new L.PosAnimation();
this._panAnim.on({
'step': this._onPanTransitionStep,
'end': this._onPanTransitionEnd
}, this);
}
// don't fire movestart if animating inertia
if (!options.noMoveStart) {
this.fire('movestart');
}
// animate pan unless animate: false specified
if (options.animate !== false) {
L.DomUtil.addClass(this._mapPane, 'leaflet-pan-anim');
var newPos = this._getMapPanePos().subtract(offset);
this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
} else {
this._rawPanBy(offset);
this.fire('move').fire('moveend');
}
return this;
},
_onPanTransitionStep: function () {
this.fire('move');
},
_onPanTransitionEnd: function () {
L.DomUtil.removeClass(this._mapPane, 'leaflet-pan-anim');
this.fire('moveend');
},
_tryAnimatedPan: function (center, options) {
// difference between the new and current centers in pixels
var offset = this._getCenterOffset(center)._floor();
// don't animate too far unless animate: true specified in options
if ((options && options.animate) !== true && !this.getSize().contains(offset)) { return false; }
this.panBy(offset, options);
return true;
}
});
/*
* L.PosAnimation fallback implementation that powers Leaflet pan animations
* in browsers that don't support CSS3 Transitions.
*/
L.PosAnimation = L.DomUtil.TRANSITION ? L.PosAnimation : L.PosAnimation.extend({
run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
this.stop();
this._el = el;
this._inProgress = true;
this._duration = duration || 0.25;
this._easeOutPower = 1 / Math.max(easeLinearity || 0.5, 0.2);
this._startPos = L.DomUtil.getPosition(el);
this._offset = newPos.subtract(this._startPos);
this._startTime = +new Date();
this.fire('start');
this._animate();
},
stop: function () {
if (!this._inProgress) { return; }
this._step();
this._complete();
},
_animate: function () {
// animation loop
this._animId = L.Util.requestAnimFrame(this._animate, this);
this._step();
},
_step: function () {
var elapsed = (+new Date()) - this._startTime,
duration = this._duration * 1000;
if (elapsed < duration) {
this._runFrame(this._easeOut(elapsed / duration));
} else {
this._runFrame(1);
this._complete();
}
},
_runFrame: function (progress) {
var pos = this._startPos.add(this._offset.multiplyBy(progress));
L.DomUtil.setPosition(this._el, pos);
this.fire('step');
},
_complete: function () {
L.Util.cancelAnimFrame(this._animId);
this._inProgress = false;
this.fire('end');
},
_easeOut: function (t) {
return 1 - Math.pow(1 - t, this._easeOutPower);
}
});
/*
* Extends L.Map to handle zoom animations.
*/
L.Map.mergeOptions({
zoomAnimation: true,
zoomAnimationThreshold: 4
});
if (L.DomUtil.TRANSITION) {
L.Map.addInitHook(function () {
// don't animate on browsers without hardware-accelerated transitions or old Android/Opera
this._zoomAnimated = this.options.zoomAnimation && L.DomUtil.TRANSITION &&
L.Browser.any3d && !L.Browser.android23 && !L.Browser.mobileOpera;
// zoom transitions run with the same duration for all layers, so if one of transitionend events
// happens after starting zoom animation (propagating to the map pane), we know that it ended globally
if (this._zoomAnimated) {
L.DomEvent.on(this._mapPane, L.DomUtil.TRANSITION_END, this._catchTransitionEnd, this);
}
});
}
L.Map.include(!L.DomUtil.TRANSITION ? {} : {
_catchTransitionEnd: function (e) {
if (this._animatingZoom && e.propertyName.indexOf('transform') >= 0) {
this._onZoomTransitionEnd();
}
},
_nothingToAnimate: function () {
return !this._container.getElementsByClassName('leaflet-zoom-animated').length;
},
_tryAnimatedZoom: function (center, zoom, options) {
if (this._animatingZoom) { return true; }
options = options || {};
// don't animate if disabled, not supported or zoom difference is too large
if (!this._zoomAnimated || options.animate === false || this._nothingToAnimate() ||
Math.abs(zoom - this._zoom) > this.options.zoomAnimationThreshold) { return false; }
// 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),
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; }
this
.fire('movestart')
.fire('zoomstart');
this._animateZoom(center, zoom, origin, scale, null, true);
return true;
},
_animateZoom: function (center, zoom, origin, scale, delta, backwards, forTouchZoom) {
if (!forTouchZoom) {
this._animatingZoom = true;
}
// put transform transition on all layers with leaflet-zoom-animated class
L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
// remember what center/zoom to set after animation
this._animateToCenter = center;
this._animateToZoom = zoom;
// disable any dragging during animation
if (L.Draggable) {
L.Draggable._disabled = true;
}
L.Util.requestAnimFrame(function () {
this.fire('zoomanim', {
center: center,
zoom: zoom,
origin: origin,
scale: scale,
delta: delta,
backwards: backwards
});
}, this);
},
_onZoomTransitionEnd: function () {
this._animatingZoom = false;
L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
this._resetView(this._animateToCenter, this._animateToZoom, true, true);
if (L.Draggable) {
L.Draggable._disabled = false;
}
}
});
/*
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,
maximumAge: 0,
enableHighAccuracy: false
},
locate: function (/*Object*/ options) {
options = this._locateOptions = L.extend(this._defaultLocateOptions, options);
if (!navigator.geolocation) {
this._handleGeolocationError({
code: 0,
message: 'Geolocation not supported.'
});
return this;
}
var onResponse = L.bind(this._handleGeolocationResponse, this),
onError = L.bind(this._handleGeolocationError, this);
if (options.watch) {
this._locationWatchId =
navigator.geolocation.watchPosition(onResponse, onError, options);
} else {
navigator.geolocation.getCurrentPosition(onResponse, onError, options);
}
return this;
},
stopLocate: function () {
if (navigator.geolocation) {
navigator.geolocation.clearWatch(this._locationWatchId);
}
if (this._locateOptions) {
this._locateOptions.setView = false;
}
return this;
},
_handleGeolocationError: function (error) {
var c = error.code,
message = error.message ||
(c === 1 ? 'permission denied' :
(c === 2 ? 'position unavailable' : 'timeout'));
if (this._locateOptions.setView && !this._loaded) {
this.fitWorld();
}
this.fire('locationerror', {
code: c,
message: 'Geolocation error: ' + message + '.'
});
},
_handleGeolocationResponse: function (pos) {
var lat = pos.coords.latitude,
lng = pos.coords.longitude,
latlng = new L.LatLng(lat, lng),
latAccuracy = 180 * pos.coords.accuracy / 40075017,
lngAccuracy = latAccuracy / Math.cos(L.LatLng.DEG_TO_RAD * lat),
bounds = L.latLngBounds(
[lat - latAccuracy, lng - lngAccuracy],
[lat + latAccuracy, lng + lngAccuracy]),
options = this._locateOptions;
if (options.setView) {
var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom);
this.setView(latlng, zoom);
}
var data = {
latlng: latlng,
bounds: bounds,
timestamp: pos.timestamp
};
for (var i in pos.coords) {
if (typeof pos.coords[i] === 'number') {
data[i] = pos.coords[i];
}
}
this.fire('locationfound', data);
}
});
}(window, document));

View File

@ -0,0 +1,478 @@
/* required styles */
.leaflet-map-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-pane,
.leaflet-tile-container,
.leaflet-overlay-pane,
.leaflet-shadow-pane,
.leaflet-marker-pane,
.leaflet-popup-pane,
.leaflet-overlay-pane svg,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
-ms-touch-action: none;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container img {
max-width: none !important;
}
/* stupid Android 2 doesn't understand "max-width: none" properly */
.leaflet-container img.leaflet-image-layer {
max-width: 15000px !important;
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-tile-pane { z-index: 2; }
.leaflet-objects-pane { z-index: 3; }
.leaflet-overlay-pane { z-index: 4; }
.leaflet-shadow-pane { z-index: 5; }
.leaflet-marker-pane { z-index: 6; }
.leaflet-popup-pane { z-index: 7; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 7;
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile,
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-tile-loaded,
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile,
.leaflet-touching .leaflet-zoom-animated {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-clickable {
cursor: pointer;
}
.leaflet-container {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-container,
.leaflet-dragging .leaflet-clickable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-control-zoom-out {
font-size: 20px;
}
.leaflet-touch .leaflet-control-zoom-in {
font-size: 22px;
}
.leaflet-touch .leaflet-control-zoom-out {
font-size: 24px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: content-box;
box-sizing: content-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
margin: 0 auto;
width: 40px;
height: 20px;
position: relative;
overflow: hidden;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@
"author": "", "author": "",
"license": "GPL", "license": "GPL",
"dependencies": { "dependencies": {
"angular": "^1.3.15" "angular": "^1.3.15",
"angular-route": "^1.3.15"
} }
} }

View File

@ -0,0 +1,35 @@
/*
* Énoncé:
* Implémenter une version basique d'un "Livre dont vous êtes le héros" avec le module ngRoute
*
* - La route par défaut devra afficher la page pages/intro
* - Chaque page du dossier pages/ sera accessible via une route du type /pages/<numPage>
* - Le contrôleur des pages devra exposer sur son $scope une méthode goTo(numPage) qui
* permettra de naviguer vers une nouvelle page.
*
*/
var Exo = angular.module('Exo', ['ngRoute']);
Exo.controller('PageCtrl', ['$scope', '$routeParams', '$location', function($scope, $routeParams, $location) {
$scope.pageUrl = '../pages/' + $routeParams.page;
$scope.goTo = function(page) {
$location.path('/pages/'+page);
};
}]);
Exo.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/pages/:page', {
controller: 'PageCtrl',
template: '<ng-include src="pageUrl"></ng-include>'
})
.otherwise('/pages/intro')
;
}]);

View File

@ -0,0 +1,27 @@
<html>
<head>
<meta charset="utf8">
<title>Exercice ngRoute</title>
<style>
body {
font-family: 'serif'
}
a[ng-click*=goTo] {
text-decoration: underline;
cursor: pointer;
color: blue;
}
</style>
</head>
<!-- Déclaration de l'application -->
<body ng-app="Exo">
<h1>La montagne de feu</h1>
<ng-view></ng-view>
<!-- Import de du framework Angular -->
<script src="../../node_modules/angular/angular.js"></script>
<script src="../../node_modules/angular-route/angular-route.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@ -1,19 +1,12 @@
/* /*
* Énoncé: * Énoncé:
* Implémenter une version basique d'un "Livre dont vous êtes le héros" avec le module ngRoute
* *
* - La route par défaut devra afficher la page pages/intro
* - Chaque page du dossier pages/ sera accessible via une route du type /pages/<numPage>
* - Le contrôleur des pages devra exposer sur son $scope une méthode goTo(numPage) qui
* permettra de naviguer vers une nouvelle page.
* *
*/ */
var Exo = angular.module('Exo', []); var Exo = angular.module('Exo', ['ngRoute']);
Exo.controller('MainCtrl', ['$scope', function($scope) {
$scope.nom = null;
$scope.prenom = null;
$scope.age = null;
$scope.sayHello = function() {
// alert('...');
};
}]);

View File

@ -1,30 +1,27 @@
<html> <html>
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<title>Exercice manipulation $scope et contrôleur</title> <title>Exercice ngRoute</title>
<style>
body {
font-family: 'serif'
}
a[ng-click*=goTo] {
text-decoration: underline;
cursor: pointer;
color: blue;
}
</style>
</head> </head>
<!-- Déclaration de l'application --> <!-- Déclaration de l'application -->
<body ng-app="Exo"> <body ng-app="Exo">
<h1>La montagne de feu</h1>
<div ng-controller="MainCtrl"> <!-- ??? -->
<label>Nom</label>
<input type="text" />
<br />
<label>Prénom</label>
<input type="text" />
<br />
<label>Age</label>
<input type="number" />
<br />
<button>Dire bonjour</button>
</div>
<!-- Import de du framework Angular --> <!-- Import de du framework Angular -->
<script src="../node_modules/angular/angular.js"></script> <script src="../node_modules/angular/angular.js"></script>
<script src="../node_modules/angular-route/angular-route.js"></script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>

View File

@ -0,0 +1,28 @@
<p>Vos deux jours de marche sont enfin terminés. Après avoir ôté votre
épée de son fourreau, vous la déposez sur le sol et vous poussez un
soupir de soulagement en vous asseyant sur un rocher couvert de
mousse pour prendre quelques instants de repos. Vous vous étirez, vous
vous frottez les yeux, puis vous levez votre regard vers la montagne au
sommet de feu. La montagne elle-même paraît menaçante. Son flanc
escarpé, face à vous, semble avoir été déchiqueté par les griffes de
quelque créature gigantesque. Il est hérissé d'à-pics rocheux aux angles
tranchants dont on a peine à croire qu'ils aient été façonnés par la
nature. Au sommet, on aperçoit une couleur d'un rouge sinistre - sans
doute l'effet d'une étrange végétation - qui a donné son nom à la
montagne. Personne, peut-être, ne saura jamais ce qui pousse là-haut,
car il est certainement impossible d'escalader ce pic.</p>
<p>Votre quête commence maintenant. De l'autre côté de la clairière, il y a
l'entrée d'une caverne sombre. Vous ramassez votre épée, vous vous
relevez et vous pensez à tous les dangers qui vous attendent. Puis, avec
détermination, vous remettez l'épée dans son fourreau et vous vous
avancez vers l'entrée de la caverne. Vous jetez un coup d'œil dans les
ténèbres et vous apercevez des parois suintantes et sombres ainsi que
des flaques d'eau sur le sol de pierre. L'air est froid et humide. Vous
allumez votre lanterne et vous faites prudemment quelques pas dans
l'obscurité. Des toiles d'araignées vous balaient le visage et vous
entendez le bruit que font sur le sol des pattes minuscules ; ce sont
probablement des rats qui prennent la fuite. Vous entrez dans la
caverne. Après avoir parcouru quelques mètres, vous arrivez à une
bifurcation. <a ng-click="goTo(71)">Irez-vous vers l'ouest</a> ou <a ng-click="goTo(278)">vers
l'est </a>?</p>

View File

@ -0,0 +1,3 @@
<p>Vous êtes revenu à la bifurcation et vous prenez la direction du nord.
<a ng-click="goTo(77)">Rendez-vous au 77</a>.
</p>

View File

@ -0,0 +1,6 @@
<p>Avec réticence, ils acceptent que vous veniez vous joindre à eux. À
mesure que vous bavardez et jouez, ils se détendent, et finalement vous
êtes tous en train de rire et de vous raconter des histoires drôles. Ils
semblent tout à fait inoffensifs. Vous pouvez jouer honnêtement ou
essayer de tricher. Si vous voulez jouer pour de bon, <a ng-click="goTo(346)">rendez-vous au 346</a>. Si vous préférez tricher, <a ng-click="goTo(91)">rendez-vous au 91</a>.
</p>

View File

@ -0,0 +1,5 @@
<p>Par chance l'épieu que vous avez lancé atteint au cœur le Vampire qui
pousse un cri de douleur. Vous bondissez sur lui et enfoncez plus
profondément l'épieu dans son corps. Ses cris d'agonie se font de plus
en plus faibles et son corps sans vie s'effondre sur le sol. <a ng-click="goTo(327)">Rendez-vous au 327</a>.
</p>

View File

@ -0,0 +1,22 @@
<p>
La porte n'est pas verrouillée et s'ouvre. La pièce que vous voyez
maintenant semble être une petite salle de torture : divers instruments
destinés à infliger des supplices sont en effet accrochés aux murs. Au
centre de la pièce, deux petites créatures bossues exercent leur
diabolique besogne sur la personne d'un Nain attaché par les poignets à
un crochet fixé au plafond. Les deux bossus le percent et le découpent
cruellement avec leurs épées. Le Nain pousse un dernier cri, puis se
tait, les yeux fermés. Ses geôliers émettent quelques exclamations
exprimant leur dépit et se tournent vers vous en vous regardant avec
colère comme si vous étiez responsable de l'évanouissement du Nain.
Vous devez agir rapidement. Vous avez le choix entre :
<ul>
<li>Refermer aussitôt la porte et poursuivre votre chemin le long du
couloir. <a ng-click="goTo(303)">Rendez-vous au 303</a></li>
<li>
Tirer votre épée et essayer de combattre les deux créatures. <a ng-click="goTo(19)">Rendez-vous au 19</a></li>
<li>
Vous précipiter vers le Nain et lui donner un coup d'épée en lançant un
grand rire sardonique pour donner le change aux tortionnaires. <a ng-click="goTo(68)">Rendez-vous au 68</a></li>
</ul></p>

View File

@ -0,0 +1,4 @@
<p>Vous sentez une pierre bouger, et derrière, vous trouvez un levier.
Allez-vous tirer le levier ou le laisser et retourner au croisement ? Si
vous tirez le levier, <a ng-click="goTo(252)">rendez-vous au 252</a>. Si vous retournez au croisement, <a ng-click="goTo(359)">rendez-vous au 359</a>.
</p>

View File

@ -0,0 +1,3 @@
<p>Vous vous retrouvez dans un petit passage étroit qui aboutit à une porte
située au nord. Vous essayez d'ouvrir cette porte. <a ng-click="goTo(49)">Rendez-vous au 49</a>.
</p>

View File

@ -0,0 +1,11 @@
<p>Vous fouillez dans votre sac à dos. Qu'y a-t-il à l'intérieur ? Vous
pouvez essayer d'utiliser un quelconque des accessoires suivants si
vous l'avez sur votre Liste d'Équipement :
<ul>
<li>Potion d'Invisibilité <a ng-click="goTo(39)">Rendez-vous au 39</a></li>
<li>L'Œil du Cyclope <a ng-click="goTo(382)">Rendez-vous au 382</a></li>
<li>Un morceau de Fromage <a ng-click="goTo(368)">Rendez-vous au 368</a></li>
<li>L'Arc à la Flèche d'Argent <a ng-click="goTo(194)">Rendez-vous au 194</a></li>
<li>Un Bâton en forme de Y <a ng-click="goTo(215)">Rendez-vous au 215</a></li>
</ul></p>

View File

@ -0,0 +1,21 @@
<p>Le passage devant vous mène vers le nord sur une bonne distance.
Vous pouvez vous reposer dans ce couloir et manger vos Provisions.
Par la suite, le passage tourne à l'ouest et se rétrécit considérablement.
Vous arrivez à une voûte de pierres qui vous oblige à vous baisser pour
la franchir. De l'autre côté de la voûte, vous vous arrêtez un moment et
vous regardez autour de vous. Vous êtes dans une grande caverne qui
s'enfonce dans l'obscurité. La caverne est partiellement éclairée par la
lumière du jour qui s'infiltre par une ouverture au-dessus de vous. Il
vous est impossible de voir où mène la caverne. Vous allumez votre
lanterne pour explorer l'endroit et vous entendez alors un grondement.
Une faible lueur vacille dans les ténèbres. Et soudain, un jet de feu
jaillit des profondeurs de la caverne en vous manquant de peu, et en
brûlant la mousse dont le mur est couvert par endroits ! Vous vous jetez
à plat ventre sur le sol et vous levez les yeux : un énorme DRAGON
émerge de l'obscurité et avance dans votre direction. Des volutes de
fumée lui sortent des narines. Sa peau d'écaillés rougeâtres brille d'une
lueur huileuse. Le monstre est long d'une quinzaine de mètres !
Comment allez-vous faire pour vous lancer à l'assaut de cette créature ?
</p>
<p>Vous tirez votre épée en vous préparant à l'attaque ? <a ng-click="goTo(152)">Rendez-vous au 152</a>. Vous fouillez dans votre mémoire pour essayer de trouver un autre moyen de le combattre ? <a ng-click="goTo(126)">Rendez-vous au 126</a>.
</p>

View File

@ -0,0 +1,8 @@
<p>Vous traversez la pièce sur la pointe des pieds et vous montez un
escalier étroit qui aboutit à un couloir.
</p>
<p>« C'était facile », pensez-vous, et, à la réflexion, vous vous demandez si
vous n'auriez pas mieux fait de fouiller les corps. Cela en valait peut-
être la peine. Si vous voulez revenir en arrière et fouiller les corps, en
commençant par le troisième, <a ng-click="goTo(148)">rendez-vous au 148</a>. Si vous préférez poursuivre votre chemin, <a ng-click="goTo(197)">rendez-vous au 197</a>.
</p>

View File

@ -0,0 +1,13 @@
<p>Au moment où votre pied touche une dalle en forme de main, vous
sentez qu'on vous serre la cheville comme dans un étau. Vous regardez
à vos pieds, et vous apercevez une main fantomatique qui s'accroche à
vous. Vous essayez de retrouver votre équilibre et vous y parvenez.
Mais à votre grande terreur, vous réalisez soudain que de chaque dalle
en forme de main s'élève une autre main semblable, et le sol est ainsi
jonché de mains décharnées qui se tendent et essayent d'attraper
quelque chose. Vous tirez votre épée et vous tentez d'en frapper la
main. Menez ce combat à son terme :
MAIN HABILETÉ: 6 ENDURANCE: 4
</p>
<p>Si vous êtes vainqueur, <a ng-click="goTo(185)">rendez-vous au 185</a>.
</p>

View File

@ -0,0 +1,15 @@
<p>Le liquide est doux et onctueux. À mesure que vous en buvez, vous
vous mettez à rayonner. Vous vous sentez à la fois euphorique et
légèrement ivre. Votre confiance en vous-même s'accroît et votre
fatigue disparaît. La bouteille contient de l'EAU SAINTE, bénie par le
Grand Prêtre de Kaynlesh-Ma. Elle vous a presque rendu toute votre
ENDURANCE. Augmentez-en le total jusqu'à un chiffre inférieur de 2
points à votre ENDURANCE de départ. (Si votre ENDURANCE était
plus élevée encore, n'y changez rien, vous êtes suffisamment fort
comme cela !) Ajoutez des points à votre total actuel d'HABILETÉ
jusqu'à obtenir un chiffre inférieur d'1 point à votre HABILETÉ de
départ. Vous avez également droit à quatre points de CHANCE
supplémentaires pour avoir fait une aussi heureuse trouvaille. Si vous
avez déjà regardé le parchemin, vous pouvez quitter la pièce en
direction du nord (<a ng-click="goTo(120)">rendez-vous au 120</a>). Sinon, vous pouvez y jeter un coup d'œil (<a ng-click="goTo(212)">et vous rendre au 212</a>), ou n'y prêter aucune attention et aller vers le nord de toute façon.
</p>

View File

@ -0,0 +1,16 @@
<p>Vous suivez le passage vers l'ouest jusqu'à ce qu'il tourne vers le sud.
Juste avant ce coude, il y a une pancarte qui indique : « En
construction. » Devant vous, les premières marches d'un escalier qui
descend. Trois marches, seulement ont été construites. Sur le sol sont
posés des pelles, des pioches et d'autres outils et lorsque vous avez
tourné le coin, ils se mettent soudain à s'agiter et à travailler pour
continuer de bâtir l'escalier. Vous voyez maintenant tous ces outils
creuser et piocher comme s'ils étaient tenus par des ouvriers invisibles.
Une chanson fredonnée s'élève et vous reconnaissez l'air de « Le
travail, c'est la Santé ». Devant ce spectacle, vous éclatez de rire. La
scène en effet est cocasse. Vous vous asseyez pour observer ces outils
magiques ; vous parlez même à certains d'entre eux. Prenez 2 points
d'ENDURANCE et 1 point d'HABILETÉ pendant que vous vous
détendez. Puis, revenez dans le passage en remontant vers le
croisement ; là, vous pouvez choisir d'aller au nord (<a ng-click="goTo(366)">rendez-vous au 366</a>) ou au sud (<a ng-click="goTo(250)">rendez-vous au 250</a>).
</p>

View File

@ -0,0 +1,4 @@
<p>Vous êtes maintenant plus riche de 8 Pièces d'Or. Vous trouvez
également 2 autres Pièces d'Or qu'il a cachées dans sa botte pour plus
de sûreté. <a ng-click="goTo(319)">Rendez-vous au 319</a>. Inscrivez votre gain en Pièces d'Or sur votre Feuille d'Aventure.
</p>

View File

@ -0,0 +1,4 @@
<p>Il ne se laissera pas amadouer. Tandis que vous faites quelques pas
autour de la pièce, d'une démarche embarrassée, il crie quelque chose
au chien. <a ng-click="goTo(249)">Rendez-vous au 249</a>.
</p>

View File

@ -0,0 +1,8 @@
<p>Vous ne voyez rien dans la pièce qui puisse vous aider dans votre
combat. Qu'allez-vous faire ?
<ul>
<li>Tirer votre épée, serrer les dents et partir à l'attaque ?
<a ng-click="goTo(142)">Rendez-vous au 142</a>.</li>
<li>Chercher dans votre sac à dos une autre arme que vous pourriez utiliser ? <a ng-click="goTo(105)">Rendez-vous au 105</a>.</li>
</ul></p>

View File

@ -0,0 +1,3 @@
<p>Vous arrivez à une autre bifurcation. Vous avez le choix entre aller au
nord (<a ng-click="goTo(285)">rendez-vous au 285</a>) ou poursuivre vers l'est (<a ng-click="goTo(78)">rendez-vous au 78</a>).
</p>

View File

@ -0,0 +1,3 @@
<p>Le passage mène vers le sud, puis vers l'est, et vous vous retrouvez
finalement à un croisement. <a ng-click="goTo(359)">Rendez-vous au 359</a>.
</p>

View File

@ -0,0 +1,10 @@
<p>Les malheureux étendus à vos pieds semblent presque heureux d'avoir
été soulagés du fardeau de la vie. Et tandis que vous les contemplez,
vous avez le sentiment que vous n'êtes pas seul à savoir qu'ils sont
morts. Vous jetez un coup d'œil autour de la pièce et vous avez le choix
entre :
<ul>
<li>Examiner les armes posées à terre <a ng-click="goTo(95)">Rendez-vous au 95</a>.</li>
<li>Vous approcher du cadavre étendu dans le coin nord-est <a ng-click="goTo(313)">Rendez-vous au 313</a>.</li>
<li>Aller voir ce qu'il y a dans les tonneaux <a ng-click="goTo(330)">Rendez-vous au 330</a>.
</li></ul></p>

View File

@ -0,0 +1,14 @@
<p>Les deux FARFADETS ivres qui se trouvent face à vous sont de toute
évidence surpris en vous voyant entrer, et ils cherchent maladroitement
leurs armes en s'efforçant de s'en saisir aussi vite que possible. Vous
devez les attaquer un par un. Lorsque vous lancerez les dés pour
déterminer votre Force d'Attaque, vous pourrez ajouter 1 point au total
obtenu, en raison de leur ivresse, et cela, à chaque assaut.
</p>
<p>HABILETÉ - ENDURANCE
Premier FARFADET 5 5
Deuxième FARFADET 4 5
</p>
<p>Si vous sortez vainqueur du combat, <a ng-click="goTo(378)">rendez-vous au 378</a>. Si vous
voulez prendre la fuite pendant la bataille, vous pouvez le faire en <a ng-click="goTo(42)">vous rendant au 42</a>.
</p>

View File

@ -0,0 +1,2 @@
<p>Vous êtes dans un passage est-ouest. Pour aller à l'est, <a ng-click="goTo(354)">rendez-vous au 354</a>. Si vous préférez l'ouest, <a ng-click="goTo(308)">rendez-vous au 308</a>.
</p>

View File

@ -0,0 +1,6 @@
<p>Lorsque vous vous approchez de lui, il se dresse dans son cercueil,
déploie sa cape et vous en enveloppe. Votre dernier souvenir, c'est la
douleur fulgurante que vous ressentez au moment où ses dents pointues
s'enfoncent dans votre cou. Vous n'auriez jamais dû vous laisser
prendre par le regard d'un VAMPIRE.
</p>

View File

@ -0,0 +1,11 @@
<p>Vous ouvrez votre sac à dos et vous y cherchez quelque chose que vous
pourriez lancer dans la caverne. Consultez votre Liste d'Équipement,
choisissez-y ce que vous lancerez et rayez l'objet de cette Liste. Si vous
n'avez pas d'Équipement, il vous faudra lancer une Pièce d'Or. Vous
jetez l'objet qui tombe sur le sol de la caverne avec un bruit sonore.
</p>
<p>
L'Ogre regarde dans la direction d'où est venu le bruit et va voir ce qui
se passe. Pendant ce temps, vous sortez en rampant et vous retournez à
la bifurcation en suivant le couloir en sens inverse. <a ng-click="goTo(269)">Rendez-vous au 269</a>.
</p>

View File

@ -0,0 +1,13 @@
<p>Au moment où vous tirez la poignée, un bruit métallique assourdissant
retentit dans le passage. Vous la repoussez frénétiquement pour arrêter
le signal d'alarme, mais il a déjà produit son effet. Vous entendez des
bruits de pas qui s'approchent dans le couloir.
<a ng-click="goTo(161)">Rendez-vous au 161</a> pour
voir ce que vous avez attiré. Notez le chiffre 12 car il vous faudra y
revenir après le combat que vous allez livrer.
</p>
<p>
Lorsque vous au rez vaincu la créature, vous pouvez soit retourner à la
bifurcation (<a ng-click="goTo(256)">rendez-vous au 256</a>), soit pousser la poignée (<a ng-click="goTo(364)">rendez-vous au 364</a>).
</p>

View File

@ -0,0 +1,3 @@
<p>Vous quittez la pièce, suivez un court passage et arrivez à un escalier
qui monte. Vous en gravissez les marches, et vous vous retrouvez
bientôt dans un couloir. <a ng-click="goTo(197)">Rendez-vous au 197</a>.<p>

View File

@ -0,0 +1,4 @@
<p>Le couloir est orienté vers l'est pendant quelques mètres, puis il tourne
au sud, revient ensuite vers l'est et se termine enfin par un cul-de-sac.
Allez-vous examiner ce cul-de-sac (<a ng-click="goTo(103)">rendez-vous alors au 103</a>), ou
préférez-vous retourner au croisement (<a ng-click="goTo(359)">et vous rendre alors au 359</a>) ?</p>

View File

@ -0,0 +1,24 @@
<p>Vous vous réveillez avec des élancements dans la tête et vous jetez un
regard autour de vous. La pièce fait environ huit mètres carrés et des
portes s'ouvrent au nord et au sud. On vous a jeté dans le coin sud-
ouest. Quatre hommes se tiennent debout, immobiles, au centre de la
pièce. Il semble tout au moins que ce sont des hommes. Leur peau a
une teinte gris-verdâtre. Leurs vêtements sont en lambeaux et tous les
quatre fixent le plafond d'un regard vide. L'un tient une massue, un
autre une faux, le troisième une hache et le dernier une pioche. Ils vous
ignorent complètement.</p>
<p>Autour de la pièce, il y a des outils de paysans (fourches, manches de
haches, bâtons en pointe, etc.) qui peuvent servir d'armes, ainsi que
deux boucliers et plusieurs tonneaux. Dans le coin nord-est, un cadavre
tient une épée dans une main et un bouclier dans l'autre. Vous vous
passez la main sur le visage pour voir si vous ne saignez pas ; à votre
grand soulagement, vous ne découvrez aucune trace de sang. Mais
lorsque vos mains ont bougé, les étranges créatures au centre de la
pièce ont baissé les yeux vers vous. Que faites-vous ?</p>
<ul>
<li>Vous essayez de leur parler ? <a ng-click="goTo(268)">Rendez-vous au 268</a></li>
<li>Vous vous relevez d'un bond et vous les attaquez avec votre épée ?
<a ng-click="goTo(282)">Rendez-vous au 282</a></li>
<li>Vous tentez tant bien que mal de vous enfuir par la porte sud ? <a ng-click="goTo(13)">Rendez-
vous au 13</a></li>
</ul>

View File

@ -0,0 +1,9 @@
<p>Vont-ils croire votre histoire d'achat de bateau ? Les Squelettes sont
plutôt simples d'esprit, aussi, lancez donc un dé. Si vous faites 1,2 ou 3,
ils vous croient et ils quittent tous le hangar par la porte aménagée dans
le mur nord, vous laissant ainsi seul avec les bateaux. Prenez 2 points
de CHANCE et <a ng-click="goTo(184)">rendez-vous au 184</a>.Si vous faites 4 ou 5, ils ne savent
que penser et envoient deux des leurs prendre les ordres tandis que les
trois autres vous surveillent avec leurs armes de fortune. <a ng-click="goTo(164)">Rendez-vous
au 164</a>.Si vous faites un 6, ils ne croient pas un mot de ce que vous
dites, et continuent d'avancer sur vous. <a ng-click="goTo(140)">Rendez-vous au 140</a>.</p>

View File

@ -0,0 +1,3 @@
<p>La porte s'ouvre sur un large couloir que vous suivez sur une certaine
distance jusqu'à ce que vous arriviez à une bifurcation. Là, vous pouvez
aller au nord (<a ng-click="goTo(138)">rendez-vous au 138</a>), ou à l'est (<a ng-click="goTo(76)">rendez-vous au 76</a>).</p>

View File

@ -0,0 +1,15 @@
<p>Vous ramassez la corde. Elle semble normale. Elle a même l'air de
pouvoir vous être très utile à l'occasion. Vous ouvrez votre sac à dos
pour l'y ranger, mais soudain, elle prend vie dans vos mains, remonte
rapidement le long de votre bras et tente de s'enrouler autour de votre
cou. Vous vous efforcez de couper la corde avec votre épée avant que
son étreinte ne se resserre. Tentez votre Chance. Si vous êtes chanceux,
vous arrivez à couper la corde et elle tombe sur le sol. Mais si vous êtes
malchanceux, la corde se resserre et vous perdez un point
d'ENDURANCE. Il vous faut à nouveau Tenter votre Chance pour
essayer de couper la corde, et vous aurez ainsi à la tenter jusqu'à ce que
vous y parveniez. A chaque échec, vous perdez 1 point
d'ENDURANCE (et vous n'oubliez pas de réduire votre total de
CHANCE). Si vous arrivez finalement à vaincre la corde, vous pouvez
poursuivre votre chemin en empruntant la porte nord. <a ng-click="goTo(73)">Rendez-vous
alors au 73</a>.</p>

View File

@ -0,0 +1,3 @@
<p>Le nom de « Farrigo Di Maggio » vous dit-il quelque chose ? Si tel
n'est pas le cas, il vous faut combattre le Dragon. <a ng-click="goTo(152)">Rendez-vous au 152</a>.
Si au contraire vous connaissez ce nom, <a ng-click="goTo(26)">rendez-vous au 26</a>.</p>

View File

@ -0,0 +1,8 @@
<p>Vos menaces ne l'impressionnent pas le moins du monde. Tandis que
vous continuez à discuter et que sa colère grandit, vous remarquez une
transformation dans son apparence. Il se redresse et paraît soudain plus
fort. Son visage et ses bras se couvrent de poils. Ses dents deviennent
pointues et acérées. Il vous faut prendre une décision rapide. Allez-
vous lui offrir 5 Pièces d'Or pour le calmer (déduisez cette somme de
votre Or et <a ng-click="goTo(272)">rendez-vous au 272</a>), ou vous préparer à l'attaquer (<a ng-click="goTo(188)">rendez-
vous au 188</a>) ?</p>

View File

@ -0,0 +1,4 @@
<p>Vous entendez un grondement et le sol se met à vibrer. La herse s'élève
lentement et avec bruit, disparaissant dans le plafond. Vous pouvez
maintenant aller jusqu'à la bifurcation. Choisirez-vous de poursuivre
vers l'ouest (<a ng-click="goTo(210)">rendez-vous au 210</a>), ou vers l'est (<a ng-click="goTo(58)">rendez-vous au 58</a>) ?</p>

View File

@ -0,0 +1,2 @@
<p>Vous revenez au bord de la rivière et vous décidez d'essayer la porte
aménagée au milieu du pan de roc. <a ng-click="goTo(104)">Rendez-vous au 104</a>.</p>

View File

@ -0,0 +1,8 @@
<p>Votre tête vous fait mal et vous vous sentez tout étourdi en vous
relevant. Les quatre hommes se mettent en mouvement et s'avancent
vers vous en file indienne, leurs armes prêtes. Vous cherchez votre
chemin à tâtons le long du mur pour essayer d'atteindre la porte du sud,
et il s'en faudra d'un cheveu que vous n'y arriviez. Votre pied glisse sur
un caillou et vous tombez par terre. Avant d'avoir pu vous relever, les
créatures sont sur vous. <a ng-click="goTo(282)">Rendez-vous au 282</a>.
</p>

View File

@ -0,0 +1,11 @@
<p>Le vieil homme vous demande combien vous misez. Vous pouvez
parier entre 1 et 20 Pièces d'Or (mais en tout cas pas plus que vous n'en
possédez !). Il vous donne les deux dés blancs avec lesquels il jouait, et
vous demande de les lancer. Jetez les dés une fois pour vous et une fois
pour le vieil homme. Si vous obtenez un total plus élevé que le sien,
vous gagnez une somme égale à celle que vous avez pariée, et que le
vieil homme doit vous payer. Si c'est son total qui est plus élevé, vous
perdez votre mise. Vous pouvez continuer aussi longtemps que vous au
rez des Pièces d'Or et partir ensuite par la porte pour revenir à la
bifurcation. Si vous gagnez, prenez 2 points d'HABILETÉ, 2
d'ENDURANCE et 2 de CHANCE. <a ng-click="goTo(280)">Rendez-vous après au 280</a>.</p>

View File

@ -0,0 +1,15 @@
<p>Vous bavardez de choses et d'autres, et ils semblent avoir très envie
d'avoir avec vous des relations amicales. Ils se sentent seuls dans cette
montagne, entourés de tant d'êtres malfaisants, et ils sont heureux de
parler à des visiteurs qui viennent d'un monde où l'on respecte les lois.
Ils vous expliquent que vous vous trouvez dans le Labyrinthe de Zagor.
Le seul moyen d'en trouver l'issue c'est d'aller encore plus loin à
l'intérieur des souterrains. Ils vous indiquent le chemin pour sortir du
labyrinthe : après avoir quitté cette pièce, vous tournerez à droite, à
droite encore, puis à gauche, ensuite vous irez tout droit... A ce
moment, ils sont moins précis. Ils ne sont pas tout à fait sûrs du chemin
qu'il convient de suivre. Si vous le désirez, vous pouvez vous restaurer
en prenant un Repas, mais vous devrez le partager avec eux et, de ce
fait, vous n'aurez droit qu'à la moitié des points d'ENDURANCE
habituels. Enfin, vous les remerciez et vous sortez de la pièce. <a ng-click="goTo(291)">Rendez-
vous au 291</a>.</p>

View File

@ -0,0 +1,2 @@
<p>Le bouclier est en bois et d'un modèle courant. Vous pouvez le garder
ou le jeter. <a ng-click="goTo(319)">Rendez-vous au 319</a>.</p>

View File

@ -0,0 +1,9 @@
<p>Vous êtes dans un couloir nord-sud qui se termine en cul-de-sac. Vous
jetez un regard autour de vous pour essayer de déceler sur la paroi
rocheuse un quelconque signe qui pourrait révéler quelque chose
d'intéressant, lorsque une pierre se détache soudain de la voûte au-
dessus de vous et vous tombe sur la tête. Bien que le choc n'ait pas été
trop rude, vous vous sentez légèrement étourdi. Vous luttez pour garder
votre conscience, mais vous n'y parvenez pas et vous vous effondrez
sur le sol. Quand vous ouvrez à nouveau les yeux, vous vous trouvez à
un croisement. <a ng-click="goTo(52)">Rendez-vous au 52</a>.</p>

View File

@ -0,0 +1,10 @@
<p>Il n'y a personne dans la pièce, et il ne semble pas qu'il y ait d'autre
issue. Au milieu, se trouve une table sur laquelle sont posés deux
casques, l'un en bronze, l'autre en fer. Tous les deux sont à peu près à
votre taille. Allez-vous essayer de vous coiffer d'un de ces casques, ou
estimez-vous que c'est prendre là un risque inutile ?</p>
<ul>
<li>Si vous essayez le casque de bronze, <a ng-click="goTo(202)">rendez-vous au 202</a></li>
<li>Si vous essayez celui de fer, <a ng-click="goTo(325)">rendez-vous au 325</a></li>
<li>Si vous décidez de revenir à la bifurcation, <a ng-click="goTo(8)">rendez-vous au 87</a></li>
</ul>

View File

@ -0,0 +1,7 @@
<p>L'Être gît recroquevillé dans un coin de la pièce. Vous allez à son
bureau et vous ouvrez la boîte. Il y a 18 Pièces d'Or à l'intérieur. Vous
pouvez les prendre ; n'oubliez pas de les inscrire sur votre Feuille
d'Aventure. Vous avez droit à 2 points de CHANCE pour avoir vaincu
la créature, et vous pouvez également vous restaurer en prenant un
Repas. Quand vous êtes prêt à repartir, vous quittez la pièce par la porte
nord. <a ng-click="goTo(360)">Rendez-vous au 360</a>.</p>

View File

@ -0,0 +1,4 @@
<p>Vous arrivez à la porte que vous parvenez à ouvrir après vous être battu
avec la serrure. Vous sortez précipitamment de la pièce en refermant la
porte derrière vous, vous respirez profondément à plusieurs reprises et
vous revenez à la bifurcation. <a ng-click="goTo(229)">Rendez-vous au 229</a>.</p>

View File

@ -0,0 +1,4 @@
<p>Vous ne trouvez pas de passage secret, mais en appuyant sur le mur,
vous entendez un déclic. Vous avez alors un étourdissement, et vous
vous effondrez sur le sol. Quand vous reprenez conscience, vous ne
reconnaissez pas l'endroit où vous êtes. <a ng-click="goTo(354)">Rendez-vous au 354</a>.</p>

View File

@ -0,0 +1,20 @@
<p>Le passage s'élargit et vous vous apercevez que vous allez bientôt
arriver dans une grande caverne. Vous entendez des bruits en
provenance de cette caverne, à quelque distance devant vous et vous
avancez avec prudence. En vous approchant, vous distinguez au loin
une immense silhouette et vous êtes passablement impressionné lorsque
vous réalisez que cet homme gigantesque doit mesurer au moins trois
mètres de haut ! Vêtu d'une tunique de cuir, le monstre, assis à une
table, est occupé à manger. La caverne fait au moins cent mètres de
long, et c'est là sans doute la demeure de ce GEANT. Il y a une grande
table et deux chaises contre l'un des murs. C'est à cette table que la
créature est assise. Le Géant est trop absorbé dans la consommation de
son repas (un énorme cochon) pour remarquer votre présence. Dans la
caverne, il y a également une paillasse, une grande fourrure qui lui sert
sans doute de couverture ou de cape, et un immense marteau de pierre
que vous ne pourriez même pas espérer remuer. Dans un coin brûle un
feu dont la fumée s'échappe par une ouverture pratiquée dans le
plafond. Il semble qu'il n'y ait pas d'autre issue dans la caverne que le
passage par lequel vous arrivez. Allez-vous affronter cette brute (<a ng-click="goTo(163)">et
vous rendre au 163</a>), ou revenir à la bifurcation (<a ng-click="goTo(351)">rendez-vous alors au
351</a>) ?</p>

View File

@ -0,0 +1,12 @@
<p>Au cours de votre aventure, vous avez trouvé différentes clés et vous
avez dû conserver certaines d'entre elles. Vous avez à présent la
possibilité d'utiliser trois de ces clés pour essayer d'ouvrir les serrures
du coffre.
Chaque clé porte un numéro distinct. Pour savoir si vous avez les
bonnes clés, faites la somme des trois chiffres qu'elles portent. Rendez-
vous ensuite au numéro que vous donne le total obtenu, et vous verrez
alors si vous avez choisi les clés qui vous ouvriront le coffre.
Si vous n'avez pas trois clés numérotées, voici venue la fin de votre
aventure. Vous n'avez plus qu'à vous asseoir sur le coffre et à pleurer,
car vous venez de comprendre qu'il va falloir recommencer à explorer
la montagne depuis le début pour trouver les clés qui vous manquent.</p>

View File

@ -0,0 +1,6 @@
<p>Il n'y a pas trace de passage secret ; en revanche, vous entendez des
bruits de pas qui viennent vers vous. Pour découvrir ce qui s'approche
ainsi, <a ng-click="goTo(161)">rendez-vous au 161</a>. Il vous faut combattre cette créature. Si vous
avez vaincu le monstre, <a ng-click="goTo(117)">rendez-vous au 117</a>. N'oubliez pas de noter ce
numéro pour savoir ou vous devrez vous rendre.
</p>

View File

@ -0,0 +1,28 @@
<p>Les Squelettes avancent sur vous et vous acculent contre la porte. Le
chef s'approche. Derrière lui marchent côte à côte deux Squelettes ; les
deux derniers suivent derrière. Il faut d'abord combattre le chef :</p>
<p>SQUELETTE HABILETÉ: 7 ENDURANCE: 5</p>
<p>Ensuite, vous aurez à vous battre contre les autres en les affrontant
deux par deux. Voici comment se dérouleront ces combats : vous au rez
deux paires de Squelettes à affronter, mais chacun des deux Squelettes,
formant une paire, vous attaquera séparément à chaque assaut. Vous
devrez choisir lequel vous voulez combattre en priorité. Vous mènerez
alors le premier assaut contre le Squelette que vous venez de choisir.
Ensuite, le deuxième Squelette vous attaquera ; vous lancerez les dés
pour mesurer votre Force d'Attaque à la manière habituelle, mais si
votre Force d'Attaque est supérieure à la sienne, vous n'aurez pas pour
autant blessé ce Squelette, vous aurez simplement évité le coup porté
par lui. En revanche, si sa Force d'Attaque est supérieure à la vôtre,
vous serez blessé selon les règles habituelles. Chaque assaut se déroule
ainsi en deux temps, et vous aurez gagné le combat lorsque le Squelette
que vous avez choisi d'affronter en premier sera vaincu. Vous n'aurez
donc qu'un seul Squelette à tuer à chaque combat par paire, mais ils
seront deux à pouvoir vous blesser.</p>
<p>HABILETÉ ENDURANCE
1re Paire :
SQUELETTE A 6 6
SQUELETTE B 5 6</p>
<p>2e Paire :
SQUELETTE A 5 5
SQUELETTE B 6 5</p>
<p>Si vous sortez vainqueur de cette bataille, <a ng-click="goTo(395)">rendez-vous au 395</a>.</p>

View File

@ -0,0 +1,6 @@
<p>Lorsque le vieil homme apprend que vous cherchez le trésor, il se met
en colère et vous ordonne de vous en aller : il n'a que faire des
aventuriers chercheurs de trésors. Son chien a senti sa colère et vous
montre les dents d'un air menaçant. Vous pouvez soit sourire au vieil
homme, le remercier et sortir par la porte sud (<a ng-click="goTo(66)">rendez-vous alors au 66</a>),
soit rester et essayer de l'amadouer (<a ng-click="goTo(111)">rendez-vous dans ce cas au 111</a>).</p>

View File

@ -0,0 +1,9 @@
<p>D'une voix tonitruante, il lance : « Pauvre imbécile ! Crois-tu que tu
peux te mesurer à mon pouvoir en utilisant cette arme dérisoire ? »
Vous continuez cependant d'avancer avec détermination. « Si c'est une
simple rossée que tu cherches, étranger, je te donnerai la dernière de ta
vie ! » ajoute-t-il, puis il disparaît et reparaît derrière vous. Vous faites
volte-face pour l'affronter, et le combat s'engage. Mais c'est une lutte à
mort. Cette fois, il vous sera impossible de prendre la fuite.</p>
<p>SORCIER HABILETÉ: 11 ENDURANCE: 18</p>
<p>Si vous êtes vainqueur, <a ng-click="gotTo(396)">rendez-vous au 396</a>.</p>

View File

@ -0,0 +1,15 @@
<p>Vous vous accroupissez sur le sable de la rive. Tandis que vous
préparez votre repas, vous remarquez que le sable bouge à deux mètres
de vous environ. Le sable s'agite de plus en plus et vous vous levez d'un
bond, votre épée à la main, Soudain, une énorme tête cylindrique jaillit
de la surface sablonneuse, puis se met à se tortiller et flaire votre odeur.
Le corps lisse et annelé d'un VER MARIN GÉANT s'élève alors, et
oscille dans votre direction. En même temps, une ouverture béante
hérissée de petites dents pointues apparaît dans ce qui lui tient lieu de
tête. 11 vous faut combattre cette créature.</p>
<p>VER MARIN HABILETÉ : 7 ENDURANCE : 7 GÉANT</p>
<p>Si vous êtes vainqueur, <a ng-click="goTo(44)">rendez-vous au 44</a>. Si vous préférez prendre la
fuite, vous ne pourrez le faire qu'après avoir mené trois assauts contre
la bête, et vous plongerez alors dans la rivière ; vous nagerez en suivant
le courant (<a ng-click="goTo(399)">rendez-vous au 399</a>), mais, dans votre fuite, vous aurez
perdu les Provisions que vous aviez commencé à manger.</p>

View File

@ -0,0 +1,8 @@
<p>La créature plonge son regard dans le vôtre et vous devenez incapable
de contrôler vos actes. Le monstre vous ordonne d'avancer. Vous vous
approchez lentement de lui, la bouche ouverte. Il vous ordonne de jeter
votre épieu à terre. Mais, tandis que vous contemplez cet épieu, vous
sentez que votre volonté vous revient brusquement et vous lancez votre
arme sur le Vampire alors que vous êtes tout près de lui. Tentez votre
Chance. Si vous êtes chanceux, <a ng-click="goTo(101)">rendez-vous au 101</a>. Si vous êtes
malchanceux, <a ng-click="goTo(217)">rendez-vous au 217</a></p>

View File

@ -0,0 +1,5 @@
<p>La boîte est tombée sur le sol pendant que vous combattiez le Serpent,
et une clé couleur de bronze en est sortie ; le chiffre 99 est gravé
dessus. Vous pouvez prendre cette clé (et l'inscrire sur votre Liste
d'Équipement), puis quitter la pièce. Ajoutez-vous un point de
CHANCE et <a ng-click="goTo(363)">rendez-vous au 363</a>.</p>

View File

@ -0,0 +1,3 @@
<p>Vous ne trouvez aucun passage secret. Vous retournez au croisement et
vous continuez vers le nord (<a ng-click="goTo(366)">rendez-vous au 366</a>), ou vers l'ouest
(<a ng-click="goTo(11)">rendez-vous au 11</a>).</p>

View File

@ -0,0 +1,5 @@
<p>Vous quittez la pièce et vous ouvrez la boîte dans le couloir. A
l'intérieur, vous trouvez une Pièce d'Or et une petite souris qui était
sans doute l'animal familier de la créature. Vous gardez la Pièce, et
vous relâchez la souris qui s'enfuit à toute allure le long du couloir.
Prenez 2 points de CHANCE et <a ng-click="goTo(208)">rendez-vous au 208</a>.</p>

View File

@ -0,0 +1,8 @@
<p>Tandis que vous fouillez le corps, vous essayez d'éviter de regarder son
visage terrifiant, aux chairs grises et décomposées. Des vers lui sortent
du nez et de la bouche. Soudain, vous faites un bond en arrière lorsque
vous vous apercevez avec stupéfaction que ses paupières s'agitent et
s'ouvrent. Vous évitez juste à temps de vous faire cruellement griffer
par un coup de ses ongles longs et pointus. Il bondit sur ses pieds et
vous dévisage, un sourire sadique au coin des lèvres. <a ng-click="goTo(230)">Rendez-vous au
230</a>.</p>

View File

@ -0,0 +1,11 @@
<p>Tandis que vous contemplez cette peinture murale vivante, vous ne
vous rendez pas compte que votre chandelle brûle rapidement. Soudain,
sa flamme vacille et s'éteint ! Vous entendez à nouveau les cris perçants
et ils deviennent si aigus qu'ils en sont bientôt insupportables. Vous
vous laissez tomber à genoux, les mains plaquées contre vos oreilles, et
vous rampez vers le mur. Dans quelle direction allez-vous ramper ?</p>
<ul>
<li>Vers le mur est ? <a ng-click="goTo(181)">Rendez-vous au 181</a></li>
<li>Vers le mur nord ? <a ng-click="goTo(265)">Rendez-vous au 265</a></li>
<li>Vers le mur ouest ? <a ng-click="goTo(355)">Rendez-vous au 355</a></li>
</ul>

View File

@ -0,0 +1,10 @@
<p>
Tandis que vous êtes assis sur le banc en train de manger, vous vous
détendez profondément et les courbatures de votre corps semblent
disparaître d'elles-mêmes. Ce lieu de repos est enchanté. Vous avez
droit à deux points d'ENDURANCE en plus de ceux que vous rend
votre repas (mais seulement si ce local n'excède pas vos points
d'ENDURANCE de départ ; vous pouvez également reprendre 1 point
DHABILETÉ si vous en avez perdu. Lorsque vous êtes l'ict à repartir,
avancez le long du couloir, et <a ng-click="goTo(367)">rendez-vous au 367</a>.
</p>

View File

@ -0,0 +1,6 @@
<p>Vous êtes à la croisée de trois chemins.</p>
<ul>
<li>Pour aller au nord ? <a ng-click="goTo(222)">Rendez-vous au 222</a></li>
<li>Pour aller à l'est ? <a ng-click="goTo(297)">Rendez-vous au 297</a></li>
<li>Pour aller au sud ? <a ng-click="goTo(133)">Rendez-vous au 133</a></li>
</ul>

View File

@ -0,0 +1,10 @@
<p>Vous gagnez du terrain sur la « turbulence » qui agite l'eau, mais à
quelques mètres de la rive nord, vous apercevez les yeux d'un sinistre
reptile qui vous observe à la surface de la rivière. Or, vous nagez droit
vers lui. Si vous jugez préférable de ne pas avoir affaire au propriétaire
des yeux en question, vous pouvez faire volte-face et nager le plus vite
possible vers la rive sud. Vous arrivez épuisé, vous perdez 1 point
d'ENDURANCE et <a ng-click="goTo(218)">vous allez au 218</a>. Sinon, vous pouvez prendre le
risque d'affronter les yeux et <a ng-click="goTo(86)">vous rendre alors au 86</a>. Enfin, vous
pouvez choisir de faire un détour qui vous rapprochera de la «
turbulence », et <a ng-click="goTo(158)">vous rendre dans ce cas au 158</a>.</p>

View File

@ -0,0 +1,4 @@
<p>Menez le combat :</p>
<p>DRAGON HABILETÉ : 10 ENDURANCE : 12</p>
<p>Si vous gagnez, <a ng-click="goTo(371)">rendez-vous au 371</a>.</p>

View File

@ -0,0 +1,8 @@
<p>Vous passez les deux épées dans votre ceinture, mais la nouvelle
semble avoir son caractère bien à elle. Elle commence par vous faire
une coupure à la jambe (vous perdez 1 point d'ENDURANCE) et
lorsque vous la retirez de votre ceinture, elle devient caoutchouteuse.
Elle est tout à fait inutile à présent, et vous la jetez à la rivière. Il
semble que le seul chemin à suivre soit désormais de descendre la
rivière en nageant vers l'est. Vous plongez donc et vous vous mettez à
nager. <a ng-click="goTo(399)">Rendez-vous au 399</a>.</p>

View File

@ -0,0 +1,3 @@
<p>Lorsque vous bougez, les yeux de la créature s'ouvrent aussitôt. Elle
vous voit et se met lentement debout.Son souffle se fait plus lourd et
elle marche vers vous. Il va falloir la combattre. <a ng-click="goTo(41)">Rendez-vous au 41</a>.</p>

View File

@ -0,0 +1,18 @@
<p>La porte se fend en deux ; vous parvenez à casser le panneau et à
entrer. Une torche est fixée à l'un des murs et éclaire une petite salle
d'armes remplie d'épées, de boucliers, de casques, de poignards, de
cuirasses et autres objets de même nature. Vous fouillez ce dépôt
d'armes mais sans rien trouver qui soit plus utile que votre épée.
Pourtant, votre regard est attiré par un bouclier rond, en fer. au centre
duquel est gravé un croissant. Si vous désirez prendre ce bouclier, il
vous aidera à détourner les coups que vous porteront les créatures
ennemies. Désormais, si une créature vous inflige une blessure lors d'un
combat pendant lequel vous utiliserez ce bouclier, vous pourrez lancer
un dé. Si vous faites un 6, la créature ne vous au ra atteint que pour la
valeur d'1 point, au lieu de 2 en temps normal. Si, pour une raison
quelconque, une créature ne vous infligeait qu'une blessure à un point,
un 6 obtenu au dé signifierait que le coup n'a pas porté du tout. Mais ce
bouclier est lourd, et pour pouvoir l'emporter, il vous faudra
abandonner une pièce de votre équipement (rectifiez alors votre Liste
d'Équipement). Vous quittez à présent la pièce et vous suivez le
couloir. <a ng-click="goTo(300)">Rendez-vous au 300</a>.</p>

View File

@ -0,0 +1,6 @@
<p>Vous essayez d'enfoncer la porte à coups d'épaule. Jetez deux dés. Si le
chiffre obtenu est égal ou inférieur au total de vos points d'HABILETÉ,
vous avez réussi et vous allez au343. Si le chiffre obtenu est supérieur à
vos points d'HABILETÉ, vous frottez votre épaule endolorie et vous
décidez de ne pas faire de seconde tentative. Retournez à la bifurcation
en <a ng-click="goTo(92)">vous rendant au 92</a>.</p>

View File

@ -0,0 +1,3 @@
<p>La porte s'ouvre sur un passage est-ouest qui s'oriente au nord au bout
de quelques mètres. Si vous voulez suivre cette direction, <a ng-click="goTo(4)">rendez-vous
au 4</a>. Si vous préférez ne pas franchir la porte, <a ng-click="goTo(329)">rendez-vous au 329</a>.</p>

View File

@ -0,0 +1,13 @@
<p>La surface de l'eau, autour de vous, se ride et s'agite comme si une main
imaginaire jetait des cailloux invisibles dans la rivière. Votre gorge se
serre soudain lorsque vous découvrez qu'il s'agit de... PIRANHAS ! Et
vous sentez déjà leurs dents pointues s'enfoncer dans votre chair. Vous
gesticulez, vous leur donner des coups de pied, vous vous défendez
avec toutes les armes dont vous disposez en essayant de les tenir à
distance assez longtemps pour pouvoir rejoindre la rive sud. Combattez
les Piranhas comme s'il s'agissait d'une seule et même créature.</p>
<p>PIRANHAS HABILETÉ: 5 ENDURANCE: 5</p>
<p>Menez cette bataille à son terme.</p>
<p>Si vous gagnez, vous réussissez à sortir de l'eau et à vous allonger, tout
pantelant, sur la rive sud. Vous pouvez alors prendre un repas. <a ng-click="goTo(218)">Rendez-
vous au 218</a>.</p>

View File

@ -0,0 +1,15 @@
<p>Vous ouvrez la porte et vous découvrez une grande pièce qui est la salle
à manger de ces mêmes créatures au visage constellé de verrues que
vous reconnaissez à présent. Assis autour d'une grande table, cinq
FARFADETS sont occupés à avaler goulûment une soupe de gésiers de
rats ; ils en bavent de bonheur. En même temps, ils se disputent
âprement pour savoir qui aura droit à ronger les os de rats qui restent au
fond de la marmite de soupe. Trop absorbés par cette grave question, ils
ne vous voient pas entrer. Vous pouvez vous montrer plein de hardiesse
et les attaquer (<a ng-click="goTo(365)">rendez-vous alors au 365</a>), ou estimer au contraire qu'il
n'y a aucun avantage à affronter cinq de ces créatures en même temps ;
dans ce cas, vous essayez de prendre la fuite. Si vous souhaitez quitter
la pièce, Tentez votre Chance. Si vous êtes chanceux, vous arrivez à
sortir sans qu'ils vous remarquent (<a ng-click="goTo(237)">rendez-vous au 237</a>), et vous n'aurez
aucune pénalité de fuite. Si vous êtes malchanceux, ils vous voient.
Préparez-vous alors à la bagarre, et <a ng-click="goTo(365)">rendez-vous au 365</a>.</p>

View File

@ -0,0 +1,10 @@
<p>Vous tirez votre épée du fourreau; l'Ogre vous a entendu et se prépare à
l'attaque :
</p>
<p>
OGRE HABILETÉ: 8 ENDURANCE: 10
</p>
<p>
Si vous êtes vainqueur, <a ng-click="goTo(50)">rendez-vous au 50</a>. Après le deuxième assaut,
vous pouvez fuir le long du corridor. (<a ng-click="goTo(269)">Rendez-vous au 269</a>.)
</p>

View File

@ -0,0 +1,3 @@
<p>Vous suivez un long passage étroit qui va vers le sud, puis vers l'est,
revient ensuite au sud, et vous mène finalement à une croisée des
chemins. <a ng-click="goTo(267)">Rendez-vous au 267</a>.</p>

View File

@ -0,0 +1,57 @@
<p>Assurez-vous bien que vous avez inscrit la référence comme il vous l'a
été indiqué à la page précédente !Vous reviendrez à ce numéro lorsque
vous en aurez fini avec la créature que vous allez affronter.En
cherchant des portes et des passages secrets, vous avez sondé, martelé,
raclé la paroi rocheuse, et tous ces bruits ont résonné dans les couloirs
de la montagne. Diverses créatures vagabondent librement dans les
souterrains, et ce vacarme a attiré l'attention de l'un des monstres
suivants.</p>
<p>Lancez un dé. Consultez ensuite la liste ci-dessous pour savoir quelle
créature vient vers vous. Combattez-la à la manière habituelle. Les
monstres errants n'ont jamais de trésor avec eux. Si vous terrassez la
créature, retournez au numéro que vous avez noté.</p>
<table>
<tr>
<td>Chiffre obtenu au dé</td>
<td>Créature</td>
<td>HABILETÉ</td>
<td>ENDURANCE</td>
</tr>
<tr>
<td>1</td>
<td>LUTIN</td>
<td>5</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>FARFADET</td>
<td>6</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>DIABLOTIN</td>
<td>6</td>
<td>4</td>
</tr>
<tr>
<td>4</td>
<td>RAT GÉANT</td>
<td>5</td>
<td>4</td>
</tr>
<tr>
<td>5</td>
<td>SQUELETTE</td>
<td>6</td>
<td>5</td>
</tr>
<tr>
<td>6</td>
<td>TROLL</td>
<td>8</td>
<td>4</td>
</tr>
</table>

View File

@ -0,0 +1,3 @@
<p>Le passage est orienté vers le nord et vous le suivez jusqu'à ce que vous
arriviez à une nouvelle bifurcation. Là, vous pouvez soit continuer vers
le nord (<a ng-click="goTo(23)">rendez-vous au 23</a>), soit aller vers l'ouest (<a ng-click="goTo(69)">rendez-vous au 69</a>).</p>

View File

@ -0,0 +1,12 @@
<p>Vous tirez votre épée et vous entrez dans la caverne. Le Géant s'arrête
soudain de mâcher, relève la tête et renifle autour de lui. Il se retourne
et vous voit approcher. Avec un rugissement sonore, il lance la carcasse
du cochon dans votre direction. Tentez votre Chance. Si vous êtes
chanceux, il vous rate. Si vous êtes malchanceux, la carcasse vous
heurte violemment et vous perdez 1 point d'ENDURANCE. Ensuite, le
Géant ramasse son marteau et s'apprête à vous frapper. Menez la
bataille à son terme.</p>
<p>GÉANT HABILETÉ: 8 ENDURANCE: 9</p>
<p>Si vous êtes vainqueur, <a ng-click="goTo(28)">rendez-vous au 28</a>. Vous pouvez prendre la
fuite, mais au bout du troisième assaut seulement ; vous sortez dans ce
cas par le passage où il ne pourra pas vous suivre (<a ng-click="goTo(351)">rendez-vous au 351</a>).</p>

View File

@ -0,0 +1,6 @@
<p>Vous vous rendez compte que les deux Squelettes qui viennent de
quitter le hangar reviendront bientôt et dévoileront votre mensonge. Il
vous faut agir vite. Allez-vous rapidement battre en retraite par la porte
qui se trouve derrière vous (<a ng-click="goTo(129)">rendez-vous alors au 129</a>), ou tirer votre
épée et attaquer les squelettes présents dans le hangar (<a ng-click="goTo(236)">et vous rendre
au 236</a>)</p>

View File

@ -0,0 +1,17 @@
<p>Le vieil homme vous remercie et lace ses bottes d'un air penaud. Vous
lui expliquez que vous ne lui voulez aucun mal et il se calme, rappelant
son chien. Il vous révèle que cet endroit est le seul passage qui mène
aux salles intérieures. Quelques années auparavant, la rivière était
entrée en crue au début du printemps, après une fonte des neiges
particulièrement impressionnante et il était alors devenu impossible de
s'approvisionner à l'extérieur. Tous les habitants des environs
mouraient de faim mais le Maître, se rendant compte qu'il avait besoin
de se défendre contre le monde du dehors, jeta un sort sur la région. Les
créatures qui échappèrent à la mort gardent désormais le passage.
Il vous pose ensuite des questions pour savoir qui vous êtes et ce que
vous cherchez. Qu'allez-vous faire ?</p>
<ul>
<li>Être franc avec lui et lui révéler l'objet de votre quête ? <a ng-click="goTo(141)">Rendez-vous au 141</a></li>
<li>Le remercier d'avoir bavardé avec vous et partir par la porte sud ? <a ng-click="goTo(66)">Rendez-vous au 66</a></li>
<li>Essayer de vous emparer des clés et aller à la porte la plus proche ? <a ng-click="goTo(249)">Rendez-vous au 249</a></li>
</ul>

View File

@ -0,0 +1,8 @@
<p>Vous tombez dans l'eau glacée et vous nagez frénétiquement pour
essayer d'atteindre la rive sud. A votre grand étonnement, le radeau
revient de lui-même à la rive sud après avoir fait demi-tour au milieu
du cours d'eau. Vous accélérez l'allure, car les remous que vous
provoquez en nageant pourraient attirer l'attention d'une quelconque
créature habitant la rivière. Jetez un dé. Si vous faites 1, 2, 3 ou 4, vous
parvenez à rejoindre la rive sain et sauf. <a ng-click="goTo(218)">Rendez-vous alors au 218</a>. Si
vous faites un 5 ou un 6, <a ng-click="goTo(158)">rendez-vous au 158</a>.</p>

View File

@ -0,0 +1,6 @@
<p>Vous découvrez une porte qui ouvre sur une bifurcation. Au nord, un
court passage se termine en impasse, et à l'est un autre couloir mène à
un nouveau croisement. Si vous décidez de franchir cette porte secrète,
<a ng-click="goTo(187)">rendez-vous au 187</a>. Si vous préférez ne pas vous aventurer dans cette
direction, refermez la porte et retournez au croisement précédent en
revenant sur vos pas. <a ng-click="goTo(359)">Rendez-vous alors au 359</a>.</p>

View File

@ -0,0 +1,13 @@
<p>Vous ouvrez la porte qui donne dans une vaste pièce. Une haute chaise
et une table massive semblent indiquer que quelqu'un ou quelque chose
d'important occupe cet endroit. Vous apercevez un coffre posé au
milieu de la pièce. Dans un coin se tient une créature de la taille d'un
homme ; elle a le visage constellé de verrues et domine une autre
créature plus petite mais d'apparence semblable. Un fouet à la main, le
CHEF DES FARFADETS est en train d'infliger une correction à son
serviteur qui gémit sous les coups. Qu'allez-vous faire ?</p>
<ul>
<li>Les attaquer tous deux ? <a ng-click="goTo(372)">Rendez-vous au 372</a></li>
<li>Bondir sur le Chef en espérant que son serviteur vous viendra en aide ? <a ng-click="goTo(65)">Rendez-vous au 65</a></li>
<li>Quitter la pièce et retourner au croisement ? <a ng-click="goTo(293)">Rendez-vous au 293</a></li>
</ul>

View File

@ -0,0 +1,4 @@
<p>Une à une, toutes les clés s'engagent et tournent dans les serrures. Vous
les avez placées comme il fallait ! Lorsque tourne la dernière clé, le
couvercle du coffre se libère et vous le soulevez. <a ng-click="goTo(400)">Rendez-vous au 400</a>
pour voir ce qu'il vous révèle.</p>

View File

@ -0,0 +1,15 @@
<p>A l'aide de l'épieu et du maillet (ou d'un maillet de fortune si vous n'en
avez pas), vous formez une croix et vous avancez vers le Vampire en
l'acculant dans un coin. Le Vampire siffle et essaye de vous attraper,
mais il ne peut s'approcher de vous. Il sera cependant difficile de lui
enfoncer l'épieu dans le cœur. Tandis que vous marchez vers lui, vous
trébuchez et tombez. Par un coup de chance, l'épieu est projeté en
avant, et atteint le monstre hurlant. Tentez votre Chance. Si vous êtes
chanceux, l'épieu transperce le cœur du Vampire. Si vous êtes
malchanceux, le Vampire est simplement écorché par le coup (enlevez-
lui 3 points d'ENDURANCE) et il vous rejette en arrière vers la porte
située à l'ouest. Si vous fuyez par cette porte, <a ng-click="goTo(380)">rendez-vous au 380</a>. Si
vous continuez à combattre, <a ng-click="goTo(144)">rendez-vous au 144</a>. Si vous avez eu de la
chance et que vous avez tué le Vampire, vous pouvez chercher son
trésor - <a ng-click="goTo(327)">rendez-vous alors au 327</a>.
</p>

Some files were not shown because too many files have changed in this diff Show More