From 01e644c2a3667a9fb5d2977a51e4730e5176b4c5 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 1 Apr 2013 13:37:05 +0200 Subject: Add Leaflet JS library --- chimere/static/leaflet/LICENSE | 23 ++ chimere/static/leaflet/README.md | 28 ++ chimere/static/leaflet/images/layers.png | Bin 0 -> 973 bytes chimere/static/leaflet/images/marker-icon.png | Bin 0 -> 1747 bytes chimere/static/leaflet/images/marker-icon@2x.png | Bin 0 -> 4033 bytes chimere/static/leaflet/images/marker-shadow.png | Bin 0 -> 797 bytes chimere/static/leaflet/leaflet.css | 457 +++++++++++++++++++++++ chimere/static/leaflet/leaflet.ie.css | 57 +++ chimere/static/leaflet/leaflet.js | 8 + 9 files changed, 573 insertions(+) create mode 100644 chimere/static/leaflet/LICENSE create mode 100644 chimere/static/leaflet/README.md create mode 100644 chimere/static/leaflet/images/layers.png create mode 100644 chimere/static/leaflet/images/marker-icon.png create mode 100644 chimere/static/leaflet/images/marker-icon@2x.png create mode 100644 chimere/static/leaflet/images/marker-shadow.png create mode 100644 chimere/static/leaflet/leaflet.css create mode 100644 chimere/static/leaflet/leaflet.ie.css create mode 100644 chimere/static/leaflet/leaflet.js diff --git a/chimere/static/leaflet/LICENSE b/chimere/static/leaflet/LICENSE new file mode 100644 index 0000000..46cd121 --- /dev/null +++ b/chimere/static/leaflet/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2010-2013, Vladimir Agafonkin +Copyright (c) 2010-2011, CloudMade +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/chimere/static/leaflet/README.md b/chimere/static/leaflet/README.md new file mode 100644 index 0000000..e974712 --- /dev/null +++ b/chimere/static/leaflet/README.md @@ -0,0 +1,28 @@ +Leaflet + +Leaflet is a modern open-source JavaScript library for **mobile-friendly interactive maps**. +It is developed by [Vladimir Agafonkin][] with a team of dedicated [contributors][]. +Weighing just about 27 KB of gzipped JS code, it has all the [features][] most developers ever need for online maps. + +Leaflet is designed with *simplicity*, *performance* and *usability* in mind. +It works efficiently across all major desktop and mobile platforms out of the box, +taking advantage of HTML5 and CSS3 on modern browsers while being accessible on older ones too. +It can also be extended with many [plugins][], +has a beautiful, easy to use and [well-documented][] API +and a simple, readable [source code][] that is a joy to [contribute][] to. + +For more information, check out the [official website][]. + +We're happy to meet new contributors. +If you want to **get involved** with Leaflet development, check out the [contribution guide][contribute]. +Let's make the best open-source library for maps that can possibly exist! + + [Vladimir Agafonkin]: http://agafonkin.com/en + [contributors]: https://github.com/Leaflet/Leaflet/graphs/contributors + [features]: http://leafletjs.com/features.html + [plugins]: http://leafletjs.com/plugins.html + [well-documented]: http://leafletjs.com/reference.html "Leaflet API reference" + [source code]: https://github.com/Leaflet/Leaflet "Leaflet GitHub repository" + [hosted on GitHub]: http://github.com/Leaflet/Leaflet + [contribute]: https://github.com/Leaflet/Leaflet/blob/master/CONTRIBUTING.md "A guide to contributing to Leaflet" + [official website]: http://leafletjs.com diff --git a/chimere/static/leaflet/images/layers.png b/chimere/static/leaflet/images/layers.png new file mode 100644 index 0000000..ef90a08 Binary files /dev/null and b/chimere/static/leaflet/images/layers.png differ diff --git a/chimere/static/leaflet/images/marker-icon.png b/chimere/static/leaflet/images/marker-icon.png new file mode 100644 index 0000000..e2e9f75 Binary files /dev/null and b/chimere/static/leaflet/images/marker-icon.png differ diff --git a/chimere/static/leaflet/images/marker-icon@2x.png b/chimere/static/leaflet/images/marker-icon@2x.png new file mode 100644 index 0000000..0015b64 Binary files /dev/null and b/chimere/static/leaflet/images/marker-icon@2x.png differ diff --git a/chimere/static/leaflet/images/marker-shadow.png b/chimere/static/leaflet/images/marker-shadow.png new file mode 100644 index 0000000..d1e773c Binary files /dev/null and b/chimere/static/leaflet/images/marker-shadow.png differ diff --git a/chimere/static/leaflet/leaflet.css b/chimere/static/leaflet/leaflet.css new file mode 100644 index 0000000..ea3da39 --- /dev/null +++ b/chimere/static/leaflet/leaflet.css @@ -0,0 +1,457 @@ +/* required styles */ + +.leaflet-map-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-pane, +.leaflet-overlay-pane, +.leaflet-shadow-pane, +.leaflet-marker-pane, +.leaflet-popup-pane, +.leaflet-overlay-pane svg, +.leaflet-zoom-box, +.leaflet-image-layer, +.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; + } +.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; + } + +.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; } + + +/* 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-dragging .leaflet-clickable, +.leaflet-dragging .leaflet-container { + 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 #05f; + background: white; + opacity: 0.5; + } + + +/* general typography */ +.leaflet-container { + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 0 8px rgba(0,0,0,0.4); + border: 1px solid #888; + -webkit-border-radius: 5px; + border-radius: 5px; + } +.leaflet-bar-part { + background-color: rgba(255, 255, 255, 0.8); + border-bottom: 1px solid #aaa; + } +.leaflet-bar-part-top { + -webkit-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; + } +.leaflet-bar-part-bottom { + -webkit-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; + border-bottom: none; + } + +.leaflet-touch .leaflet-bar { + -webkit-border-radius: 10px; + border-radius: 10px; + } +.leaflet-touch .leaflet-bar-part { + border-bottom: 4px solid rgba(0,0,0,0.3); + } +.leaflet-touch .leaflet-bar-part-top { + -webkit-border-radius: 7px 7px 0 0; + border-radius: 7px 7px 0 0; + } +.leaflet-touch .leaflet-bar-part-bottom { + -webkit-border-radius: 0 0 7px 7px; + border-radius: 0 0 7px 7px; + border-bottom: none; + } + + +/* zoom control */ + +.leaflet-container .leaflet-control-zoom { + margin-left: 13px; + margin-top: 12px; + } +.leaflet-control-zoom a { + width: 22px; + height: 22px; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-control-zoom a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-control-zoom a:hover { + background-color: #fff; + color: #777; + } +.leaflet-control-zoom-in { + font: bold 18px/24px Arial, Helvetica, sans-serif; + } +.leaflet-control-zoom-out { + font: bold 23px/20px Tahoma, Verdana, sans-serif; + } +.leaflet-control-zoom a.leaflet-control-zoom-disabled { + cursor: default; + background-color: rgba(255, 255, 255, 0.8); + color: #bbb; + } + +.leaflet-touch .leaflet-control-zoom a { + width: 30px; + height: 30px; + } +.leaflet-touch .leaflet-control-zoom-in { + font-size: 24px; + line-height: 29px; + } +.leaflet-touch .leaflet-control-zoom-out { + font-size: 28px; + line-height: 24px; + } + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 7px rgba(0,0,0,0.4); + background: #f8f8f9; + -webkit-border-radius: 8px; + border-radius: 8px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.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-color: rgba(255, 255, 255, 0.7); + box-shadow: 0 0 5px #bbb; + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + } +.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; + color: black; + line-height: 1.1; + padding: 2px 5px 1px; + font-size: 11px; + text-shadow: 1px 1px 1px #fff; + background-color: rgba(255, 255, 255, 0.5); + box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.2); + white-space: nowrap; + overflow: hidden; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + } +.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-control-zoom { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-control-zoom { + border: 4px solid rgba(0,0,0,0.3); + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + -webkit-border-radius: 20px; + border-radius: 20px; + } +.leaflet-popup-content { + margin: 14px 20px; + 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: 15px; + height: 15px; + padding: 1px; + + margin: -8px 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 5px 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; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } +.leaflet-editing-icon { + -webkit-border-radius: 2px; + border-radius: 2px; + } diff --git a/chimere/static/leaflet/leaflet.ie.css b/chimere/static/leaflet/leaflet.ie.css new file mode 100644 index 0000000..b59c814 --- /dev/null +++ b/chimere/static/leaflet/leaflet.ie.css @@ -0,0 +1,57 @@ +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + +.leaflet-control { + display: inline; + } + +.leaflet-popup-tip { + width: 21px; + _width: 27px; + margin: 0 auto; + _margin-top: -3px; + + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + } +.leaflet-popup-tip-container { + margin-top: -1px; + } +.leaflet-popup-content-wrapper, .leaflet-popup-tip { + border: 1px solid #999; + } +.leaflet-popup-content-wrapper { + zoom: 1; + } + +.leaflet-control-zoom, +.leaflet-control-layers { + border: 3px solid #999; + } +.leaflet-control-zoom a { + background-color: #eee; + } +.leaflet-control-zoom a:hover { + background-color: #fff; + } +.leaflet-control-layers-toggle { + } +.leaflet-control-attribution, +.leaflet-control-layers, +.leaflet-control-scale-line { + background: white; + } +.leaflet-zoom-box { + filter: alpha(opacity=50); + } +.leaflet-control-attribution { + border-top: 1px solid #bbb; + border-left: 1px solid #bbb; + } diff --git a/chimere/static/leaflet/leaflet.js b/chimere/static/leaflet/leaflet.js new file mode 100644 index 0000000..bdf0b69 --- /dev/null +++ b/chimere/static/leaflet/leaflet.js @@ -0,0 +1,8 @@ +/* + Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com + (c) 2010-2013, Vladimir Agafonkin, CloudMade +*/ +(function(t,e,i){var n,o;typeof exports!=i+""?n=exports:(o=t.L,n={},n.noConflict=function(){return t.L=o,this},t.L=n),n.version="0.5.1",n.Util={extend:function(t){var e,i,n,o,s=Array.prototype.slice.call(arguments,1);for(i=0,n=s.length;n>i;i++){o=s[i]||{};for(e in o)o.hasOwnProperty(e)&&(t[e]=o[e])}return t},bind:function(t,e){var i=arguments.length>2?Array.prototype.slice.call(arguments,2):null;return function(){return t.apply(e,i||arguments)}},stamp:function(){var t=0,e="_leaflet_id";return function(i){return i[e]=i[e]||++t,i[e]}}(),limitExecByInterval:function(t,e,n){var o,s;return function a(){var r=arguments;return o?(s=!0,i):(o=!0,setTimeout(function(){o=!1,s&&(a.apply(n,r),s=!1)},e),t.apply(n,r),i)}},falseFn:function(){return!1},formatNum:function(t,e){var i=Math.pow(10,e||5);return Math.round(t*i)/i},splitWords:function(t){return t.replace(/^\s+|\s+$/g,"").split(/\s+/)},setOptions:function(t,e){return t.options=n.extend({},t.options,e),t.options},getParamString:function(t,e){var i=[];for(var n in t)t.hasOwnProperty(n)&&i.push(n+"="+t[n]);return(e&&-1!==e.indexOf("?")?"&":"?")+i.join("&")},template:function(t,e){return t.replace(/\{ *([\w_]+) *\}/g,function(t,i){var n=e[i];if(!e.hasOwnProperty(i))throw Error("No value provided for variable "+t);return n})},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},emptyImageUrl:"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="},function(){function e(e){var i,n,o=["webkit","moz","o","ms"];for(i=0;o.length>i&&!n;i++)n=t[o[i]+e];return n}function o(e){var i=+new Date,n=Math.max(0,16-(i-s));return s=i+n,t.setTimeout(e,n)}var s=0,a=t.requestAnimationFrame||e("RequestAnimationFrame")||o,r=t.cancelAnimationFrame||e("CancelAnimationFrame")||e("CancelRequestAnimationFrame")||function(e){t.clearTimeout(e)};n.Util.requestAnimFrame=function(e,s,r,h){return e=n.bind(e,s),r&&a===o?(e(),i):a.call(t,e,h)},n.Util.cancelAnimFrame=function(e){e&&r.call(t,e)}}(),n.extend=n.Util.extend,n.bind=n.Util.bind,n.stamp=n.Util.stamp,n.setOptions=n.Util.setOptions,n.Class=function(){},n.Class.extend=function(t){var e=function(){this.initialize&&this.initialize.apply(this,arguments),this._initHooks&&this.callInitHooks()},i=function(){};i.prototype=this.prototype;var o=new i;o.constructor=e,e.prototype=o;for(var s in this)this.hasOwnProperty(s)&&"prototype"!==s&&(e[s]=this[s]);t.statics&&(n.extend(e,t.statics),delete t.statics),t.includes&&(n.Util.extend.apply(null,[o].concat(t.includes)),delete t.includes),t.options&&o.options&&(t.options=n.extend({},o.options,t.options)),n.extend(o,t),o._initHooks=[];var a=this;return o.callInitHooks=function(){if(!this._initHooksCalled){a.prototype.callInitHooks&&a.prototype.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;e>t;t++)o._initHooks[t].call(this)}},e},n.Class.include=function(t){n.extend(this.prototype,t)},n.Class.mergeOptions=function(t){n.extend(this.prototype.options,t)},n.Class.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i)};var s="_leaflet_events";n.Mixin={},n.Mixin.Events={addEventListener:function(t,e,i){var o,a,r,h=this[s]=this[s]||{};if("object"==typeof t){for(o in t)t.hasOwnProperty(o)&&this.addEventListener(o,t[o],e);return this}for(t=n.Util.splitWords(t),a=0,r=t.length;r>a;a++)h[t[a]]=h[t[a]]||[],h[t[a]].push({action:e,context:i||this});return this},hasEventListeners:function(t){return s in this&&t in this[s]&&this[s][t].length>0},removeEventListener:function(t,e,i){var o,a,r,h,l,u=this[s];if("object"==typeof t){for(o in t)t.hasOwnProperty(o)&&this.removeEventListener(o,t[o],e);return this}for(t=n.Util.splitWords(t),a=0,r=t.length;r>a;a++)if(this.hasEventListeners(t[a]))for(h=u[t[a]],l=h.length-1;l>=0;l--)e&&h[l].action!==e||i&&h[l].context!==i||h.splice(l,1);return this},fireEvent:function(t,e){if(!this.hasEventListeners(t))return this;for(var i=n.extend({type:t,target:this},e),o=this[s][t].slice(),a=0,r=o.length;r>a;a++)o[a].action.call(o[a].context||this,i);return this}},n.Mixin.Events.on=n.Mixin.Events.addEventListener,n.Mixin.Events.off=n.Mixin.Events.removeEventListener,n.Mixin.Events.fire=n.Mixin.Events.fireEvent,function(){var o=!!t.ActiveXObject,s=o&&!t.XMLHttpRequest,a=o&&!e.querySelector,r=navigator.userAgent.toLowerCase(),h=-1!==r.indexOf("webkit"),l=-1!==r.indexOf("chrome"),u=-1!==r.indexOf("android"),c=-1!==r.search("android [23]"),_=typeof orientation!=i+"",d=t.navigator&&t.navigator.msPointerEnabled&&t.navigator.msMaxTouchPoints,p="devicePixelRatio"in t&&t.devicePixelRatio>1||"matchMedia"in t&&t.matchMedia("(min-resolution:144dpi)")&&t.matchMedia("(min-resolution:144dpi)").matches,m=e.documentElement,f=o&&"transition"in m.style,g="WebKitCSSMatrix"in t&&"m11"in new t.WebKitCSSMatrix,v="MozPerspective"in m.style,y="OTransition"in m.style,L=!t.L_DISABLE_3D&&(f||g||v||y),P=!t.L_NO_TOUCH&&function(){var t="ontouchstart";if(d||t in m)return!0;var i=e.createElement("div"),n=!1;return i.setAttribute?(i.setAttribute(t,"return;"),"function"==typeof i[t]&&(n=!0),i.removeAttribute(t),i=null,n):!1}();n.Browser={ie:o,ie6:s,ie7:a,webkit:h,android:u,android23:c,chrome:l,ie3d:f,webkit3d:g,gecko3d:v,opera3d:y,any3d:L,mobile:_,mobileWebkit:_&&h,mobileWebkit3d:_&&g,mobileOpera:_&&t.opera,touch:P,msTouch:d,retina:p}}(),n.Point=function(t,e,i){this.x=i?Math.round(t):t,this.y=i?Math.round(e):e},n.Point.prototype={clone:function(){return new n.Point(this.x,this.y)},add:function(t){return this.clone()._add(n.point(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(n.point(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},distanceTo:function(t){t=n.point(t);var e=t.x-this.x,i=t.y-this.y;return Math.sqrt(e*e+i*i)},equals:function(t){return t.x===this.x&&t.y===this.y},toString:function(){return"Point("+n.Util.formatNum(this.x)+", "+n.Util.formatNum(this.y)+")"}},n.point=function(t,e,i){return t instanceof n.Point?t:n.Util.isArray(t)?new n.Point(t[0],t[1]):isNaN(t)?t:new n.Point(t,e,i)},n.Bounds=function(t,e){if(t)for(var i=e?[t,e]:t,n=0,o=i.length;o>n;n++)this.extend(i[n])},n.Bounds.prototype={extend:function(t){return t=n.point(t),this.min||this.max?(this.min.x=Math.min(t.x,this.min.x),this.max.x=Math.max(t.x,this.max.x),this.min.y=Math.min(t.y,this.min.y),this.max.y=Math.max(t.y,this.max.y)):(this.min=t.clone(),this.max=t.clone()),this},getCenter:function(t){return new n.Point((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return new n.Point(this.min.x,this.max.y)},getTopRight:function(){return new n.Point(this.max.x,this.min.y)},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var e,i;return t="number"==typeof t[0]||t instanceof n.Point?n.point(t):n.bounds(t),t instanceof n.Bounds?(e=t.min,i=t.max):e=i=t,e.x>=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=n.bounds(t);var e=this.min,i=this.max,o=t.min,s=t.max,a=s.x>=e.x&&o.x<=i.x,r=s.y>=e.y&&o.y<=i.y;return a&&r},isValid:function(){return!(!this.min||!this.max)}},n.bounds=function(t,e){return!t||t instanceof n.Bounds?t:new n.Bounds(t,e)},n.Transformation=function(t,e,i,n){this._a=t,this._b=e,this._c=i,this._d=n},n.Transformation.prototype={transform:function(t,e){return this._transform(t.clone(),e)},_transform:function(t,e){return e=e||1,t.x=e*(this._a*t.x+this._b),t.y=e*(this._c*t.y+this._d),t},untransform:function(t,e){return e=e||1,new n.Point((t.x/e-this._b)/this._a,(t.y/e-this._d)/this._c)}},n.DomUtil={get:function(t){return"string"==typeof t?e.getElementById(t):t},getStyle:function(t,i){var n=t.style[i];if(!n&&t.currentStyle&&(n=t.currentStyle[i]),(!n||"auto"===n)&&e.defaultView){var o=e.defaultView.getComputedStyle(t,null);n=o?o[i]:null}return"auto"===n?null:n},getViewportOffset:function(t){var i,o=0,s=0,a=t,r=e.body,h=n.Browser.ie7;do{if(o+=a.offsetTop||0,s+=a.offsetLeft||0,o+=parseInt(n.DomUtil.getStyle(a,"borderTopWidth"),10)||0,s+=parseInt(n.DomUtil.getStyle(a,"borderLeftWidth"),10)||0,i=n.DomUtil.getStyle(a,"position"),a.offsetParent===r&&"absolute"===i)break;if("fixed"===i){o+=r.scrollTop||0,s+=r.scrollLeft||0;break}a=a.offsetParent}while(a);a=t;do{if(a===r)break;o-=a.scrollTop||0,s-=a.scrollLeft||0,n.DomUtil.documentIsLtr()||!n.Browser.webkit&&!h||(s+=a.scrollWidth-a.clientWidth,h&&"hidden"!==n.DomUtil.getStyle(a,"overflow-y")&&"hidden"!==n.DomUtil.getStyle(a,"overflow")&&(s+=17)),a=a.parentNode}while(a);return new n.Point(s,o)},documentIsLtr:function(){return n.DomUtil._docIsLtrCached||(n.DomUtil._docIsLtrCached=!0,n.DomUtil._docIsLtr="ltr"===n.DomUtil.getStyle(e.body,"direction")),n.DomUtil._docIsLtr},create:function(t,i,n){var o=e.createElement(t);return o.className=i,n&&n.appendChild(o),o},disableTextSelection:function(){e.selection&&e.selection.empty&&e.selection.empty(),this._onselectstart||(this._onselectstart=e.onselectstart||null,e.onselectstart=n.Util.falseFn)},enableTextSelection:function(){e.onselectstart===n.Util.falseFn&&(e.onselectstart=this._onselectstart,this._onselectstart=null)},hasClass:function(t,e){return t.className.length>0&&RegExp("(^|\\s)"+e+"(\\s|$)").test(t.className)},addClass:function(t,e){n.DomUtil.hasClass(t,e)||(t.className+=(t.className?" ":"")+e)},removeClass:function(t,e){function i(t,i){return i===e?"":t}t.className=t.className.replace(/(\S+)\s*/g,i).replace(/(^\s+|\s+$)/,"")},setOpacity:function(t,e){if("opacity"in t.style)t.style.opacity=e;else if("filter"in t.style){var i=!1,n="DXImageTransform.Microsoft.Alpha";try{i=t.filters.item(n)}catch(o){}e=Math.round(100*e),i?(i.Enabled=100!==e,i.Opacity=e):t.style.filter+=" progid:"+n+"(opacity="+e+")"}},testProp:function(t){for(var i=e.documentElement.style,n=0;t.length>n;n++)if(t[n]in i)return t[n];return!1},getTranslateString:function(t){var e=n.Browser.webkit3d,i="translate"+(e?"3d":"")+"(",o=(e?",0":"")+")";return i+t.x+"px,"+t.y+"px"+o},getScaleString:function(t,e){var i=n.DomUtil.getTranslateString(e.add(e.multiplyBy(-1*t))),o=" scale("+t+") ";return i+o},setPosition:function(t,e,i){t._leaflet_pos=e,!i&&n.Browser.any3d?(t.style[n.DomUtil.TRANSFORM]=n.DomUtil.getTranslateString(e),n.Browser.mobileWebkit3d&&(t.style.WebkitBackfaceVisibility="hidden")):(t.style.left=e.x+"px",t.style.top=e.y+"px")},getPosition:function(t){return t._leaflet_pos}},n.DomUtil.TRANSFORM=n.DomUtil.testProp(["transform","WebkitTransform","OTransform","MozTransform","msTransform"]),n.DomUtil.TRANSITION=n.DomUtil.testProp(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),n.DomUtil.TRANSITION_END="webkitTransition"===n.DomUtil.TRANSITION||"OTransition"===n.DomUtil.TRANSITION?n.DomUtil.TRANSITION+"End":"transitionend",n.LatLng=function(t,e){var i=parseFloat(t),n=parseFloat(e);if(isNaN(i)||isNaN(n))throw Error("Invalid LatLng object: ("+t+", "+e+")");this.lat=i,this.lng=n},n.extend(n.LatLng,{DEG_TO_RAD:Math.PI/180,RAD_TO_DEG:180/Math.PI,MAX_MARGIN:1e-9}),n.LatLng.prototype={equals:function(t){if(!t)return!1;t=n.latLng(t);var e=Math.max(Math.abs(this.lat-t.lat),Math.abs(this.lng-t.lng));return n.LatLng.MAX_MARGIN>=e},toString:function(t){return"LatLng("+n.Util.formatNum(this.lat,t)+", "+n.Util.formatNum(this.lng,t)+")"},distanceTo:function(t){t=n.latLng(t);var e=6378137,i=n.LatLng.DEG_TO_RAD,o=(t.lat-this.lat)*i,s=(t.lng-this.lng)*i,a=this.lat*i,r=t.lat*i,h=Math.sin(o/2),l=Math.sin(s/2),u=h*h+l*l*Math.cos(a)*Math.cos(r);return 2*e*Math.atan2(Math.sqrt(u),Math.sqrt(1-u))},wrap:function(t,e){var i=this.lng;return t=t||-180,e=e||180,i=(i+e)%(e-t)+(t>i||i===e?e:t),new n.LatLng(this.lat,i)}},n.latLng=function(t,e){return t instanceof n.LatLng?t:n.Util.isArray(t)?new n.LatLng(t[0],t[1]):isNaN(t)?t:new n.LatLng(t,e)},n.LatLngBounds=function(t,e){if(t)for(var i=e?[t,e]:t,n=0,o=i.length;o>n;n++)this.extend(i[n])},n.LatLngBounds.prototype={extend:function(t){return t="number"==typeof t[0]||"string"==typeof t[0]||t instanceof n.LatLng?n.latLng(t):n.latLngBounds(t),t instanceof n.LatLng?this._southWest||this._northEast?(this._southWest.lat=Math.min(t.lat,this._southWest.lat),this._southWest.lng=Math.min(t.lng,this._southWest.lng),this._northEast.lat=Math.max(t.lat,this._northEast.lat),this._northEast.lng=Math.max(t.lng,this._northEast.lng)):(this._southWest=new n.LatLng(t.lat,t.lng),this._northEast=new n.LatLng(t.lat,t.lng)):t instanceof n.LatLngBounds&&(this.extend(t._southWest),this.extend(t._northEast)),this},pad:function(t){var e=this._southWest,i=this._northEast,o=Math.abs(e.lat-i.lat)*t,s=Math.abs(e.lng-i.lng)*t;return new n.LatLngBounds(new n.LatLng(e.lat-o,e.lng-s),new n.LatLng(i.lat+o,i.lng+s))},getCenter:function(){return new n.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 n.LatLng(this._northEast.lat,this._southWest.lng)},getSouthEast:function(){return new n.LatLng(this._southWest.lat,this._northEast.lng)},contains:function(t){t="number"==typeof t[0]||t instanceof n.LatLng?n.latLng(t):n.latLngBounds(t);var e,i,o=this._southWest,s=this._northEast;return t instanceof n.LatLngBounds?(e=t.getSouthWest(),i=t.getNorthEast()):e=i=t,e.lat>=o.lat&&i.lat<=s.lat&&e.lng>=o.lng&&i.lng<=s.lng},intersects:function(t){t=n.latLngBounds(t);var e=this._southWest,i=this._northEast,o=t.getSouthWest(),s=t.getNorthEast(),a=s.lat>=e.lat&&o.lat<=i.lat,r=s.lng>=e.lng&&o.lng<=i.lng;return a&&r},toBBoxString:function(){var t=this._southWest,e=this._northEast;return[t.lng,t.lat,e.lng,e.lat].join(",")},equals:function(t){return t?(t=n.latLngBounds(t),this._southWest.equals(t.getSouthWest())&&this._northEast.equals(t.getNorthEast())):!1},isValid:function(){return!(!this._southWest||!this._northEast)}},n.latLngBounds=function(t,e){return!t||t instanceof n.LatLngBounds?t:new n.LatLngBounds(t,e)},n.Projection={},n.Projection.SphericalMercator={MAX_LATITUDE:85.0511287798,project:function(t){var e=n.LatLng.DEG_TO_RAD,i=this.MAX_LATITUDE,o=Math.max(Math.min(i,t.lat),-i),s=t.lng*e,a=o*e;return a=Math.log(Math.tan(Math.PI/4+a/2)),new n.Point(s,a)},unproject:function(t){var e=n.LatLng.RAD_TO_DEG,i=t.x*e,o=(2*Math.atan(Math.exp(t.y))-Math.PI/2)*e;return new n.LatLng(o,i)}},n.Projection.LonLat={project:function(t){return new n.Point(t.lng,t.lat)},unproject:function(t){return new n.LatLng(t.y,t.x)}},n.CRS={latLngToPoint:function(t,e){var i=this.projection.project(t),n=this.scale(e);return this.transformation._transform(i,n)},pointToLatLng:function(t,e){var i=this.scale(e),n=this.transformation.untransform(t,i);return this.projection.unproject(n)},project:function(t){return this.projection.project(t)},scale:function(t){return 256*Math.pow(2,t)}},n.CRS.Simple=n.extend({},n.CRS,{projection:n.Projection.LonLat,transformation:new n.Transformation(1,0,-1,0),scale:function(t){return Math.pow(2,t)}}),n.CRS.EPSG3857=n.extend({},n.CRS,{code:"EPSG:3857",projection:n.Projection.SphericalMercator,transformation:new n.Transformation(.5/Math.PI,.5,-.5/Math.PI,.5),project:function(t){var e=this.projection.project(t),i=6378137;return e.multiplyBy(i)}}),n.CRS.EPSG900913=n.extend({},n.CRS.EPSG3857,{code:"EPSG:900913"}),n.CRS.EPSG4326=n.extend({},n.CRS,{code:"EPSG:4326",projection:n.Projection.LonLat,transformation:new n.Transformation(1/360,.5,-1/360,.5)}),n.Map=n.Class.extend({includes:n.Mixin.Events,options:{crs:n.CRS.EPSG3857,fadeAnimation:n.DomUtil.TRANSITION&&!n.Browser.android23,trackResize:!0,markerZoomAnimation:n.DomUtil.TRANSITION&&n.Browser.any3d},initialize:function(t,e){e=n.setOptions(this,e),this._initContainer(t),this._initLayout(),this.callInitHooks(),this._initEvents(),e.maxBounds&&this.setMaxBounds(e.maxBounds),e.center&&e.zoom!==i&&this.setView(n.latLng(e.center),e.zoom,!0),this._initLayers(e.layers)},setView:function(t,e){return this._resetView(n.latLng(t),this._limitZoom(e)),this},setZoom:function(t){return this.setView(this.getCenter(),t)},zoomIn:function(t){return this.setZoom(this._zoom+(t||1))},zoomOut:function(t){return this.setZoom(this._zoom-(t||1))},fitBounds:function(t){var e=this.getBoundsZoom(t);return this.setView(n.latLngBounds(t).getCenter(),e)},fitWorld:function(){var t=new n.LatLng(-60,-170),e=new n.LatLng(85,179);return this.fitBounds(new n.LatLngBounds(t,e))},panTo:function(t){return this.setView(t,this._zoom)},panBy:function(t){return this.fire("movestart"),this._rawPanBy(n.point(t)),this.fire("move"),this.fire("moveend")},setMaxBounds:function(t){if(t=n.latLngBounds(t),this.options.maxBounds=t,!t)return this._boundsMinZoom=null,this;var e=this.getBoundsZoom(t,!0);return this._boundsMinZoom=e,this._loaded&&(e>this._zoom?this.setView(t.getCenter(),e):this.panInsideBounds(t)),this},panInsideBounds:function(t){t=n.latLngBounds(t);var e=this.getBounds(),i=this.project(e.getSouthWest()),o=this.project(e.getNorthEast()),s=this.project(t.getSouthWest()),a=this.project(t.getNorthEast()),r=0,h=0;return o.ya.x&&(r=a.x-o.x),i.y>s.y&&(h=s.y-i.y),i.x=r);return c&&e?null:e?r:r-1},getSize:function(){return(!this._size||this._sizeChanged)&&(this._size=new n.Point(this._container.clientWidth,this._container.clientHeight),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(){var t=this._getTopLeftPoint();return new n.Bounds(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._initialTopLeftPoint},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t){var e=this.options.crs;return e.scale(t)/e.scale(this._zoom)},getScaleZoom:function(t){return this._zoom+Math.log(t)/Math.LN2},project:function(t,e){return e=e===i?this._zoom:e,this.options.crs.latLngToPoint(n.latLng(t),e)},unproject:function(t,e){return e=e===i?this._zoom:e,this.options.crs.pointToLatLng(n.point(t),e)},layerPointToLatLng:function(t){var e=n.point(t).add(this._initialTopLeftPoint);return this.unproject(e)},latLngToLayerPoint:function(t){var e=this.project(n.latLng(t))._round();return e._subtract(this._initialTopLeftPoint)},containerPointToLayerPoint:function(t){return n.point(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return n.point(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var e=this.containerPointToLayerPoint(n.point(t));return this.layerPointToLatLng(e)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(n.latLng(t)))},mouseEventToContainerPoint:function(t){return n.DomEvent.getMousePosition(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var e=this._container=n.DomUtil.get(t);if(e._leaflet)throw Error("Map container is already initialized.");e._leaflet=!0},_initLayout:function(){var t=this._container;n.DomUtil.addClass(t,"leaflet-container"),n.Browser.touch&&n.DomUtil.addClass(t,"leaflet-touch"),this.options.fadeAnimation&&n.DomUtil.addClass(t,"leaflet-fade-anim");var e=n.DomUtil.getStyle(t,"position");"absolute"!==e&&"relative"!==e&&"fixed"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._mapPane=t.mapPane=this._createPane("leaflet-map-pane",this._container),this._tilePane=t.tilePane=this._createPane("leaflet-tile-pane",this._mapPane),t.objectsPane=this._createPane("leaflet-objects-pane",this._mapPane),t.shadowPane=this._createPane("leaflet-shadow-pane"),t.overlayPane=this._createPane("leaflet-overlay-pane"),t.markerPane=this._createPane("leaflet-marker-pane"),t.popupPane=this._createPane("leaflet-popup-pane");var e=" leaflet-zoom-hide";this.options.markerZoomAnimation||(n.DomUtil.addClass(t.markerPane,e),n.DomUtil.addClass(t.shadowPane,e),n.DomUtil.addClass(t.popupPane,e))},_createPane:function(t,e){return n.DomUtil.create("div",t,e||this._panes.objectsPane)},_initLayers:function(t){t=t?n.Util.isArray(t)?t:[t]:[],this._layers={},this._zoomBoundLayers={},this._tileLayersNum=0;var e,i;for(e=0,i=t.length;i>e;e++)this.addLayer(t[e])},_resetView:function(t,e,i,o){var s=this._zoom!==e;o||(this.fire("movestart"),s&&this.fire("zoomstart")),this._zoom=e,this._initialTopLeftPoint=this._getNewTopLeftPoint(t),i?this._initialTopLeftPoint._add(this._getMapPanePos()):n.DomUtil.setPosition(this._mapPane,new n.Point(0,0)),this._tileLayersToLoad=this._tileLayersNum;var a=!this._loaded;this._loaded=!0,this.fire("viewreset",{hard:!i}),this.fire("move"),(s||o)&&this.fire("zoomend"),this.fire("moveend",{hard:!i}),a&&this.fire("load")},_rawPanBy:function(t){n.DomUtil.setPosition(this._mapPane,this._getMapPanePos().subtract(t))},_updateZoomLevels:function(){var t,e=1/0,n=-1/0;for(t in this._zoomBoundLayers)if(this._zoomBoundLayers.hasOwnProperty(t)){var o=this._zoomBoundLayers[t];isNaN(o.options.minZoom)||(e=Math.min(e,o.options.minZoom)),isNaN(o.options.maxZoom)||(n=Math.max(n,o.options.maxZoom))}t===i?this._layersMaxZoom=this._layersMinZoom=i:(this._layersMaxZoom=n,this._layersMinZoom=e)},_initEvents:function(){if(n.DomEvent){n.DomEvent.on(this._container,"click",this._onMouseClick,this);var e,i,o=["dblclick","mousedown","mouseup","mouseenter","mouseleave","mousemove","contextmenu"];for(e=0,i=o.length;i>e;e++)n.DomEvent.on(this._container,o[e],this._fireMouseEvent,this);this.options.trackResize&&n.DomEvent.on(t,"resize",this._onResize,this)}},_onResize:function(){n.Util.cancelAnimFrame(this._resizeRequest),this._resizeRequest=n.Util.requestAnimFrame(this.invalidateSize,this,!1,this._container)},_onMouseClick:function(t){!this._loaded||this.dragging&&this.dragging.moved()||(this.fire("preclick"),this._fireMouseEvent(t))},_fireMouseEvent:function(t){if(this._loaded){var e=t.type;if(e="mouseenter"===e?"mouseover":"mouseleave"===e?"mouseout":e,this.hasEventListeners(e)){"contextmenu"===e&&n.DomEvent.preventDefault(t);var i=this.mouseEventToContainerPoint(t),o=this.containerPointToLayerPoint(i),s=this.layerPointToLatLng(o);this.fire(e,{latlng:s,layerPoint:o,containerPoint:i,originalEvent:t})}}},_onTileLayerLoad:function(){this._tileLayersToLoad--,this._tileLayersNum&&!this._tileLayersToLoad&&this._tileBg&&(clearTimeout(this._clearTileBgTimer),this._clearTileBgTimer=setTimeout(n.bind(this._clearTileBg,this),500))},whenReady:function(t,e){return this._loaded?t.call(e||this,this):this.on("load",t,e),this},_getMapPanePos:function(){return n.DomUtil.getPosition(this._mapPane)},_getTopLeftPoint:function(){if(!this._loaded)throw Error("Set map center and zoom first.");return this._initialTopLeftPoint.subtract(this._getMapPanePos())},_getNewTopLeftPoint:function(t,e){var i=this.getSize()._divideBy(2);return this.project(t,e)._subtract(i)._round()},_latLngToNewLayerPoint:function(t,e,i){var n=this._getNewTopLeftPoint(i,e).add(this._getMapPanePos());return this.project(t,e)._subtract(n)},_getCenterLayerPoint:function(){return this.containerPointToLayerPoint(this.getSize()._divideBy(2))},_getCenterOffset:function(t){return this.latLngToLayerPoint(t).subtract(this._getCenterLayerPoint())},_limitZoom:function(t){var e=this.getMinZoom(),i=this.getMaxZoom();return Math.max(e,Math.min(i,t))}}),n.map=function(t,e){return new n.Map(t,e)},n.Projection.Mercator={MAX_LATITUDE:85.0840591556,R_MINOR:6356752.3142,R_MAJOR:6378137,project:function(t){var e=n.LatLng.DEG_TO_RAD,i=this.MAX_LATITUDE,o=Math.max(Math.min(i,t.lat),-i),s=this.R_MAJOR,a=this.R_MINOR,r=t.lng*e*s,h=o*e,l=a/s,u=Math.sqrt(1-l*l),c=u*Math.sin(h);c=Math.pow((1-c)/(1+c),.5*u);var _=Math.tan(.5*(.5*Math.PI-h))/c;return h=-a*Math.log(_),new n.Point(r,h)},unproject:function(t){for(var e,i=n.LatLng.RAD_TO_DEG,o=this.R_MAJOR,s=this.R_MINOR,a=t.x*i/o,r=s/o,h=Math.sqrt(1-r*r),l=Math.exp(-t.y/s),u=Math.PI/2-2*Math.atan(l),c=15,_=1e-7,d=c,p=.1;Math.abs(p)>_&&--d>0;)e=h*Math.sin(u),p=Math.PI/2-2*Math.atan(l*Math.pow((1-e)/(1+e),.5*h))-u,u+=p;return new n.LatLng(u*i,a)}},n.CRS.EPSG3395=n.extend({},n.CRS,{code:"EPSG:3395",projection:n.Projection.Mercator,transformation:function(){var t=n.Projection.Mercator,e=t.R_MAJOR,i=t.R_MINOR;return new n.Transformation(.5/(Math.PI*e),.5,-.5/(Math.PI*i),.5)}()}),n.TileLayer=n.Class.extend({includes:n.Mixin.Events,options:{minZoom:0,maxZoom:18,tileSize:256,subdomains:"abc",errorTileUrl:"",attribution:"",zoomOffset:0,opacity:1,unloadInvisibleTiles:n.Browser.mobile,updateWhenIdle:n.Browser.mobile},initialize:function(t,e){e=n.setOptions(this,e),e.detectRetina&&n.Browser.retina&&e.maxZoom>0&&(e.tileSize=Math.floor(e.tileSize/2),e.zoomOffset++,e.minZoom>0&&e.minZoom--,this.options.maxZoom--),this._url=t;var i=this.options.subdomains;"string"==typeof i&&(this.options.subdomains=i.split(""))},onAdd:function(t){this._map=t,this._initContainer(),this._createTileProto(),t.on({viewreset:this._resetCallback,moveend:this._update},this),this.options.updateWhenIdle||(this._limitedUpdate=n.Util.limitExecByInterval(this._update,150,this),t.on("move",this._limitedUpdate,this)),this._reset(),this._update()},addTo:function(t){return t.addLayer(this),this},onRemove:function(t){this._container.parentNode.removeChild(this._container),t.off({viewreset:this._resetCallback,moveend:this._update},this),this.options.updateWhenIdle||t.off("move",this._limitedUpdate,this),this._container=null,this._map=null},bringToFront:function(){var t=this._map._panes.tilePane;return this._container&&(t.appendChild(this._container),this._setAutoZIndex(t,Math.max)),this},bringToBack:function(){var t=this._map._panes.tilePane;return this._container&&(t.insertBefore(this._container,t.firstChild),this._setAutoZIndex(t,Math.min)),this},getAttribution:function(){return this.options.attribution},setOpacity:function(t){return this.options.opacity=t,this._map&&this._updateOpacity(),this},setZIndex:function(t){return this.options.zIndex=t,this._updateZIndex(),this},setUrl:function(t,e){return this._url=t,e||this.redraw(),this},redraw:function(){return this._map&&(this._map._panes.tilePane.empty=!1,this._reset(!0),this._update()),this},_updateZIndex:function(){this._container&&this.options.zIndex!==i&&(this._container.style.zIndex=this.options.zIndex)},_setAutoZIndex:function(t,e){var i,n,o,s=t.children,a=-e(1/0,-1/0);for(n=0,o=s.length;o>n;n++)s[n]!==this._container&&(i=parseInt(s[n].style.zIndex,10),isNaN(i)||(a=e(a,i)));this.options.zIndex=this._container.style.zIndex=(isFinite(a)?a:0)+e(1,-1)},_updateOpacity:function(){n.DomUtil.setOpacity(this._container,this.options.opacity);var t,e=this._tiles;if(n.Browser.webkit)for(t in e)e.hasOwnProperty(t)&&(e[t].style.webkitTransform+=" translate(0,0)")},_initContainer:function(){var t=this._map._panes.tilePane;(!this._container||t.empty)&&(this._container=n.DomUtil.create("div","leaflet-layer"),this._updateZIndex(),t.appendChild(this._container),1>this.options.opacity&&this._updateOpacity())},_resetCallback:function(t){this._reset(t.hard)},_reset:function(t){var e=this._tiles;for(var i in e)e.hasOwnProperty(i)&&this.fire("tileunload",{tile:e[i]});this._tiles={},this._tilesToLoad=0,this.options.reuseTiles&&(this._unusedTiles=[]),t&&this._container&&(this._container.innerHTML=""),this._initContainer()},_update:function(){if(this._map){var t=this._map.getPixelBounds(),e=this._map.getZoom(),i=this.options.tileSize;if(!(e>this.options.maxZoom||this.options.minZoom>e)){var o=new n.Point(Math.floor(t.min.x/i),Math.floor(t.min.y/i)),s=new n.Point(Math.floor(t.max.x/i),Math.floor(t.max.y/i)),a=new n.Bounds(o,s);this._addTilesFromCenterOut(a),(this.options.unloadInvisibleTiles||this.options.reuseTiles)&&this._removeOtherTiles(a)}}},_addTilesFromCenterOut:function(t){var i,o,s,a=[],r=t.getCenter();for(i=t.min.y;t.max.y>=i;i++)for(o=t.min.x;t.max.x>=o;o++)s=new n.Point(o,i),this._tileShouldBeLoaded(s)&&a.push(s);var h=a.length;if(0!==h){a.sort(function(t,e){return t.distanceTo(r)-e.distanceTo(r)});var l=e.createDocumentFragment();for(this._tilesToLoad||this.fire("loading"),this._tilesToLoad+=h,o=0;h>o;o++)this._addTile(a[o],l);this._container.appendChild(l)}},_tileShouldBeLoaded:function(t){if(t.x+":"+t.y in this._tiles)return!1;if(!this.options.continuousWorld){var e=this._getWrapTileNum();if(this.options.noWrap&&(0>t.x||t.x>=e)||0>t.y||t.y>=e)return!1}return!0},_removeOtherTiles:function(t){var e,i,n,o;for(o in this._tiles)this._tiles.hasOwnProperty(o)&&(e=o.split(":"),i=parseInt(e[0],10),n=parseInt(e[1],10),(t.min.x>i||i>t.max.x||t.min.y>n||n>t.max.y)&&this._removeTile(o))},_removeTile:function(t){var e=this._tiles[t];this.fire("tileunload",{tile:e,url:e.src}),this.options.reuseTiles?(n.DomUtil.removeClass(e,"leaflet-tile-loaded"),this._unusedTiles.push(e)):e.parentNode===this._container&&this._container.removeChild(e),n.Browser.android||(e.src=n.Util.emptyImageUrl),delete this._tiles[t]},_addTile:function(t,e){var i=this._getTilePos(t),o=this._getTile();n.DomUtil.setPosition(o,i,n.Browser.chrome||n.Browser.android23),this._tiles[t.x+":"+t.y]=o,this._loadTile(o,t),o.parentNode!==this._container&&e.appendChild(o) +},_getZoomForUrl:function(){var t=this.options,e=this._map.getZoom();return t.zoomReverse&&(e=t.maxZoom-e),e+t.zoomOffset},_getTilePos:function(t){var e=this._map.getPixelOrigin(),i=this.options.tileSize;return t.multiplyBy(i).subtract(e)},getTileUrl:function(t){return this._adjustTilePoint(t),n.Util.template(this._url,n.extend({s:this._getSubdomain(t),z:this._getZoomForUrl(),x:t.x,y:t.y},this.options))},_getWrapTileNum:function(){return Math.pow(2,this._getZoomForUrl())},_adjustTilePoint:function(t){var e=this._getWrapTileNum();this.options.continuousWorld||this.options.noWrap||(t.x=(t.x%e+e)%e),this.options.tms&&(t.y=e-t.y-1)},_getSubdomain:function(t){var e=(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[e]},_createTileProto:function(){var t=this._tileImg=n.DomUtil.create("img","leaflet-tile");t.style.width=t.style.height=this.options.tileSize+"px",t.galleryimg="no"},_getTile:function(){if(this.options.reuseTiles&&this._unusedTiles.length>0){var t=this._unusedTiles.pop();return this._resetTile(t),t}return this._createTile()},_resetTile:function(){},_createTile:function(){var t=this._tileImg.cloneNode(!1);return t.onselectstart=t.onmousemove=n.Util.falseFn,t},_loadTile:function(t,e){t._layer=this,t.onload=this._tileOnLoad,t.onerror=this._tileOnError,t.src=this.getTileUrl(e)},_tileLoaded:function(){this._tilesToLoad--,this._tilesToLoad||this.fire("load")},_tileOnLoad:function(){var t=this._layer;this.src!==n.Util.emptyImageUrl&&(n.DomUtil.addClass(this,"leaflet-tile-loaded"),t.fire("tileload",{tile:this,url:this.src})),t._tileLoaded()},_tileOnError:function(){var t=this._layer;t.fire("tileerror",{tile:this,url:this.src});var e=t.options.errorTileUrl;e&&(this.src=e),t._tileLoaded()}}),n.tileLayer=function(t,e){return new n.TileLayer(t,e)},n.TileLayer.WMS=n.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",version:"1.1.1",layers:"",styles:"",format:"image/jpeg",transparent:!1},initialize:function(t,e){this._url=t;var i=n.extend({},this.defaultWmsParams);i.width=i.height=e.detectRetina&&n.Browser.retina?2*this.options.tileSize:this.options.tileSize;for(var o in e)this.options.hasOwnProperty(o)||(i[o]=e[o]);this.wmsParams=i,n.setOptions(this,e)},onAdd:function(t){var e=parseFloat(this.wmsParams.version)>=1.3?"crs":"srs";this.wmsParams[e]=t.options.crs.code,n.TileLayer.prototype.onAdd.call(this,t)},getTileUrl:function(t,e){this._adjustTilePoint(t);var i=this._map,o=i.options.crs,s=this.options.tileSize,a=t.multiplyBy(s),r=a.add(new n.Point(s,s)),h=o.project(i.unproject(a,e)),l=o.project(i.unproject(r,e)),u=[h.x,l.y,l.x,h.y].join(","),c=n.Util.template(this._url,{s:this._getSubdomain(t)});return c+n.Util.getParamString(this.wmsParams,c)+"&bbox="+u},setParams:function(t,e){return n.extend(this.wmsParams,t),e||this.redraw(),this}}),n.tileLayer.wms=function(t,e){return new n.TileLayer.WMS(t,e)},n.TileLayer.Canvas=n.TileLayer.extend({options:{async:!1},initialize:function(t){n.setOptions(this,t)},redraw:function(){var t=this._tiles;for(var e in t)t.hasOwnProperty(e)&&this._redrawTile(t[e])},_redrawTile:function(t){this.drawTile(t,t._tilePoint,this._map._zoom)},_createTileProto:function(){var t=this._canvasProto=n.DomUtil.create("canvas","leaflet-tile");t.width=t.height=this.options.tileSize},_createTile:function(){var t=this._canvasProto.cloneNode(!1);return t.onselectstart=t.onmousemove=n.Util.falseFn,t},_loadTile:function(t,e){t._layer=this,t._tilePoint=e,this._redrawTile(t),this.options.async||this.tileDrawn(t)},drawTile:function(){},tileDrawn:function(t){this._tileOnLoad.call(t)}}),n.tileLayer.canvas=function(t){return new n.TileLayer.Canvas(t)},n.ImageOverlay=n.Class.extend({includes:n.Mixin.Events,options:{opacity:1},initialize:function(t,e,i){this._url=t,this._bounds=n.latLngBounds(e),n.setOptions(this,i)},onAdd:function(t){this._map=t,this._image||this._initImage(),t._panes.overlayPane.appendChild(this._image),t.on("viewreset",this._reset,this),t.options.zoomAnimation&&n.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._image),t.off("viewreset",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},setOpacity:function(t){return this.options.opacity=t,this._updateOpacity(),this},bringToFront:function(){return this._image&&this._map._panes.overlayPane.appendChild(this._image),this},bringToBack:function(){var t=this._map._panes.overlayPane;return this._image&&t.insertBefore(this._image,t.firstChild),this},_initImage:function(){this._image=n.DomUtil.create("img","leaflet-image-layer"),this._map.options.zoomAnimation&&n.Browser.any3d?n.DomUtil.addClass(this._image,"leaflet-zoom-animated"):n.DomUtil.addClass(this._image,"leaflet-zoom-hide"),this._updateOpacity(),n.extend(this._image,{galleryimg:"no",onselectstart:n.Util.falseFn,onmousemove:n.Util.falseFn,onload:n.bind(this._onImageLoad,this),src:this._url})},_animateZoom:function(t){var e=this._map,i=this._image,o=e.getZoomScale(t.zoom),s=this._bounds.getNorthWest(),a=this._bounds.getSouthEast(),r=e._latLngToNewLayerPoint(s,t.zoom,t.center),h=e._latLngToNewLayerPoint(a,t.zoom,t.center)._subtract(r),l=r._add(h._multiplyBy(.5*(1-1/o)));i.style[n.DomUtil.TRANSFORM]=n.DomUtil.getTranslateString(l)+" scale("+o+") "},_reset:function(){var t=this._image,e=this._map.latLngToLayerPoint(this._bounds.getNorthWest()),i=this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(e);n.DomUtil.setPosition(t,e),t.style.width=i.x+"px",t.style.height=i.y+"px"},_onImageLoad:function(){this.fire("load")},_updateOpacity:function(){n.DomUtil.setOpacity(this._image,this.options.opacity)}}),n.imageOverlay=function(t,e,i){return new n.ImageOverlay(t,e,i)},n.Icon=n.Class.extend({options:{className:""},initialize:function(t){n.setOptions(this,t)},createIcon:function(){return this._createIcon("icon")},createShadow:function(){return this._createIcon("shadow")},_createIcon:function(t){var e=this._getIconUrl(t);if(!e){if("icon"===t)throw Error("iconUrl not set in Icon options (see the docs).");return null}var i=this._createImg(e);return this._setIconStyles(i,t),i},_setIconStyles:function(t,e){var i,o=this.options,s=n.point(o[e+"Size"]);i="shadow"===e?n.point(o.shadowAnchor||o.iconAnchor):n.point(o.iconAnchor),!i&&s&&(i=s.divideBy(2,!0)),t.className="leaflet-marker-"+e+" "+o.className,i&&(t.style.marginLeft=-i.x+"px",t.style.marginTop=-i.y+"px"),s&&(t.style.width=s.x+"px",t.style.height=s.y+"px")},_createImg:function(t){var i;return n.Browser.ie6?(i=e.createElement("div"),i.style.filter='progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+t+'")'):(i=e.createElement("img"),i.src=t),i},_getIconUrl:function(t){return n.Browser.retina&&this.options[t+"RetinaUrl"]?this.options[t+"RetinaUrl"]:this.options[t+"Url"]}}),n.icon=function(t){return new n.Icon(t)},n.Icon.Default=n.Icon.extend({options:{iconSize:new n.Point(25,41),iconAnchor:new n.Point(12,41),popupAnchor:new n.Point(1,-34),shadowSize:new n.Point(41,41)},_getIconUrl:function(t){var e=t+"Url";if(this.options[e])return this.options[e];n.Browser.retina&&"icon"===t&&(t+="@2x");var i=n.Icon.Default.imagePath;if(!i)throw Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually.");return i+"/marker-"+t+".png"}}),n.Icon.Default.imagePath=function(){var t,i,n,o,s=e.getElementsByTagName("script"),a=/\/?leaflet[\-\._]?([\w\-\._]*)\.js\??/;for(t=0,i=s.length;i>t;t++)if(n=s[t].src,o=n.match(a))return n.split(a)[0]+"/images"}(),n.Marker=n.Class.extend({includes:n.Mixin.Events,options:{icon:new n.Icon.Default,title:"",clickable:!0,draggable:!1,zIndexOffset:0,opacity:1,riseOnHover:!1,riseOffset:250},initialize:function(t,e){n.setOptions(this,e),this._latlng=n.latLng(t)},onAdd:function(t){this._map=t,t.on("viewreset",this.update,this),this._initIcon(),this.update(),t.options.zoomAnimation&&t.options.markerZoomAnimation&&t.on("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},onRemove:function(t){this._removeIcon(),this.fire("remove"),t.off({viewreset:this.update,zoomanim:this._animateZoom},this),this._map=null},getLatLng:function(){return this._latlng},setLatLng:function(t){return this._latlng=n.latLng(t),this.update(),this.fire("move",{latlng:this._latlng})},setZIndexOffset:function(t){return this.options.zIndexOffset=t,this.update(),this},setIcon:function(t){return this._map&&this._removeIcon(),this.options.icon=t,this._map&&(this._initIcon(),this.update()),this},update:function(){if(this._icon){var t=this._map.latLngToLayerPoint(this._latlng).round();this._setPos(t)}return this},_initIcon:function(){var t=this.options,e=this._map,i=e.options.zoomAnimation&&e.options.markerZoomAnimation,o=i?"leaflet-zoom-animated":"leaflet-zoom-hide",s=!1;this._icon||(this._icon=t.icon.createIcon(),t.title&&(this._icon.title=t.title),this._initInteraction(),s=1>this.options.opacity,n.DomUtil.addClass(this._icon,o),t.riseOnHover&&n.DomEvent.on(this._icon,"mouseover",this._bringToFront,this).on(this._icon,"mouseout",this._resetZIndex,this)),this._shadow||(this._shadow=t.icon.createShadow(),this._shadow&&(n.DomUtil.addClass(this._shadow,o),s=1>this.options.opacity)),s&&this._updateOpacity();var a=this._map._panes;a.markerPane.appendChild(this._icon),this._shadow&&a.shadowPane.appendChild(this._shadow)},_removeIcon:function(){var t=this._map._panes;this.options.riseOnHover&&n.DomEvent.off(this._icon,"mouseover",this._bringToFront).off(this._icon,"mouseout",this._resetZIndex),t.markerPane.removeChild(this._icon),this._shadow&&t.shadowPane.removeChild(this._shadow),this._icon=this._shadow=null},_setPos:function(t){n.DomUtil.setPosition(this._icon,t),this._shadow&&n.DomUtil.setPosition(this._shadow,t),this._zIndex=t.y+this.options.zIndexOffset,this._resetZIndex()},_updateZIndex:function(t){this._icon.style.zIndex=this._zIndex+t},_animateZoom:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center);this._setPos(e)},_initInteraction:function(){if(this.options.clickable){var t=this._icon,e=["dblclick","mousedown","mouseover","mouseout","contextmenu"];n.DomUtil.addClass(t,"leaflet-clickable"),n.DomEvent.on(t,"click",this._onMouseClick,this);for(var i=0;e.length>i;i++)n.DomEvent.on(t,e[i],this._fireMouseEvent,this);n.Handler.MarkerDrag&&(this.dragging=new n.Handler.MarkerDrag(this),this.options.draggable&&this.dragging.enable())}},_onMouseClick:function(t){var e=this.dragging&&this.dragging.moved();(this.hasEventListeners(t.type)||e)&&n.DomEvent.stopPropagation(t),e||(this.dragging&&this.dragging._enabled||!this._map.dragging||!this._map.dragging.moved())&&this.fire(t.type,{originalEvent:t})},_fireMouseEvent:function(t){this.fire(t.type,{originalEvent:t}),"contextmenu"===t.type&&this.hasEventListeners(t.type)&&n.DomEvent.preventDefault(t),"mousedown"!==t.type&&n.DomEvent.stopPropagation(t)},setOpacity:function(t){this.options.opacity=t,this._map&&this._updateOpacity()},_updateOpacity:function(){n.DomUtil.setOpacity(this._icon,this.options.opacity),this._shadow&&n.DomUtil.setOpacity(this._shadow,this.options.opacity)},_bringToFront:function(){this._updateZIndex(this.options.riseOffset)},_resetZIndex:function(){this._updateZIndex(0)}}),n.marker=function(t,e){return new n.Marker(t,e)},n.DivIcon=n.Icon.extend({options:{iconSize:new n.Point(12,12),className:"leaflet-div-icon"},createIcon:function(){var t=e.createElement("div"),i=this.options;return i.html&&(t.innerHTML=i.html),i.bgPos&&(t.style.backgroundPosition=-i.bgPos.x+"px "+-i.bgPos.y+"px"),this._setIconStyles(t,"icon"),t},createShadow:function(){return null}}),n.divIcon=function(t){return new n.DivIcon(t)},n.Map.mergeOptions({closePopupOnClick:!0}),n.Popup=n.Class.extend({includes:n.Mixin.Events,options:{minWidth:50,maxWidth:300,maxHeight:null,autoPan:!0,closeButton:!0,offset:new n.Point(0,6),autoPanPadding:new n.Point(5,5),className:"",zoomAnimation:!0},initialize:function(t,e){n.setOptions(this,t),this._source=e,this._animated=n.Browser.any3d&&this.options.zoomAnimation},onAdd:function(t){this._map=t,this._container||this._initLayout(),this._updateContent();var e=t.options.fadeAnimation;e&&n.DomUtil.setOpacity(this._container,0),t._panes.popupPane.appendChild(this._container),t.on("viewreset",this._updatePosition,this),this._animated&&t.on("zoomanim",this._zoomAnimation,this),t.options.closePopupOnClick&&t.on("preclick",this._close,this),this._update(),e&&n.DomUtil.setOpacity(this._container,1)},addTo:function(t){return t.addLayer(this),this},openOn:function(t){return t.openPopup(this),this},onRemove:function(t){t._panes.popupPane.removeChild(this._container),n.Util.falseFn(this._container.offsetWidth),t.off({viewreset:this._updatePosition,preclick:this._close,zoomanim:this._zoomAnimation},this),t.options.fadeAnimation&&n.DomUtil.setOpacity(this._container,0),this._map=null},setLatLng:function(t){return this._latlng=n.latLng(t),this._update(),this},setContent:function(t){return this._content=t,this._update(),this},_close:function(){var t=this._map;t&&(t._popup=null,t.removeLayer(this).fire("popupclose",{popup:this}))},_initLayout:function(){var t,e="leaflet-popup",i=e+" "+this.options.className+" leaflet-zoom-"+(this._animated?"animated":"hide"),o=this._container=n.DomUtil.create("div",i);this.options.closeButton&&(t=this._closeButton=n.DomUtil.create("a",e+"-close-button",o),t.href="#close",t.innerHTML="×",n.DomEvent.on(t,"click",this._onCloseButtonClick,this));var s=this._wrapper=n.DomUtil.create("div",e+"-content-wrapper",o);n.DomEvent.disableClickPropagation(s),this._contentNode=n.DomUtil.create("div",e+"-content",s),n.DomEvent.on(this._contentNode,"mousewheel",n.DomEvent.stopPropagation),this._tipContainer=n.DomUtil.create("div",e+"-tip-container",o),this._tip=n.DomUtil.create("div",e+"-tip",this._tipContainer)},_update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updateLayout(),this._updatePosition(),this._container.style.visibility="",this._adjustPan())},_updateContent:function(){if(this._content){if("string"==typeof this._content)this._contentNode.innerHTML=this._content;else{for(;this._contentNode.hasChildNodes();)this._contentNode.removeChild(this._contentNode.firstChild);this._contentNode.appendChild(this._content)}this.fire("contentupdate")}},_updateLayout:function(){var t=this._contentNode,e=t.style;e.width="",e.whiteSpace="nowrap";var i=t.offsetWidth;i=Math.min(i,this.options.maxWidth),i=Math.max(i,this.options.minWidth),e.width=i+1+"px",e.whiteSpace="",e.height="";var o=t.offsetHeight,s=this.options.maxHeight,a="leaflet-popup-scrolled";s&&o>s?(e.height=s+"px",n.DomUtil.addClass(t,a)):n.DomUtil.removeClass(t,a),this._containerWidth=this._container.offsetWidth},_updatePosition:function(){if(this._map){var t=this._map.latLngToLayerPoint(this._latlng),e=this._animated,i=this.options.offset;e&&n.DomUtil.setPosition(this._container,t),this._containerBottom=-i.y-(e?0:t.y),this._containerLeft=-Math.round(this._containerWidth/2)+i.x+(e?0:t.x),this._container.style.bottom=this._containerBottom+"px",this._container.style.left=this._containerLeft+"px"}},_zoomAnimation:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center);n.DomUtil.setPosition(this._container,e)},_adjustPan:function(){if(this.options.autoPan){var t=this._map,e=this._container.offsetHeight,i=this._containerWidth,o=new n.Point(this._containerLeft,-e-this._containerBottom);this._animated&&o._add(n.DomUtil.getPosition(this._container));var s=t.layerPointToContainerPoint(o),a=this.options.autoPanPadding,r=t.getSize(),h=0,l=0;0>s.x&&(h=s.x-a.x),s.x+i>r.x&&(h=s.x+i-r.x+a.x),0>s.y&&(l=s.y-a.y),s.y+e>r.y&&(l=s.y+e-r.y+a.y),(h||l)&&t.panBy(new n.Point(h,l))}},_onCloseButtonClick:function(t){this._close(),n.DomEvent.stop(t)}}),n.popup=function(t,e){return new n.Popup(t,e)},n.Marker.include({openPopup:function(){return this._popup&&this._map&&(this._popup.setLatLng(this._latlng),this._map.openPopup(this._popup)),this},closePopup:function(){return this._popup&&this._popup._close(),this},bindPopup:function(t,e){var i=n.point(this.options.icon.options.popupAnchor)||new n.Point(0,0);return i=i.add(n.Popup.prototype.options.offset),e&&e.offset&&(i=i.add(e.offset)),e=n.extend({offset:i},e),this._popup||this.on("click",this.openPopup,this).on("remove",this.closePopup,this).on("move",this._movePopup,this),this._popup=new n.Popup(e,this).setContent(t),this},unbindPopup:function(){return this._popup&&(this._popup=null,this.off("click",this.openPopup).off("remove",this.closePopup).off("move",this._movePopup)),this},_movePopup:function(t){this._popup.setLatLng(t.latlng)}}),n.Map.include({openPopup:function(t){return this.closePopup(),this._popup=t,this.addLayer(t).fire("popupopen",{popup:this._popup})},closePopup:function(){return this._popup&&this._popup._close(),this}}),n.LayerGroup=n.Class.extend({initialize:function(t){this._layers={};var e,i;if(t)for(e=0,i=t.length;i>e;e++)this.addLayer(t[e])},addLayer:function(t){var e=n.stamp(t);return this._layers[e]=t,this._map&&this._map.addLayer(t),this},removeLayer:function(t){var e=n.stamp(t);return delete this._layers[e],this._map&&this._map.removeLayer(t),this},clearLayers:function(){return this.eachLayer(this.removeLayer,this),this},invoke:function(t){var e,i,n=Array.prototype.slice.call(arguments,1);for(e in this._layers)this._layers.hasOwnProperty(e)&&(i=this._layers[e],i[t]&&i[t].apply(i,n));return this},onAdd:function(t){this._map=t,this.eachLayer(t.addLayer,t)},onRemove:function(t){this.eachLayer(t.removeLayer,t),this._map=null},addTo:function(t){return t.addLayer(this),this},eachLayer:function(t,e){for(var i in this._layers)this._layers.hasOwnProperty(i)&&t.call(e,this._layers[i])},setZIndex:function(t){return this.invoke("setZIndex",t)}}),n.layerGroup=function(t){return new n.LayerGroup(t)},n.FeatureGroup=n.LayerGroup.extend({includes:n.Mixin.Events,statics:{EVENTS:"click dblclick mouseover mouseout mousemove contextmenu"},addLayer:function(t){return this._layers[n.stamp(t)]?this:(t.on(n.FeatureGroup.EVENTS,this._propagateEvent,this),n.LayerGroup.prototype.addLayer.call(this,t),this._popupContent&&t.bindPopup&&t.bindPopup(this._popupContent,this._popupOptions),this.fire("layeradd",{layer:t}))},removeLayer:function(t){return t.off(n.FeatureGroup.EVENTS,this._propagateEvent,this),n.LayerGroup.prototype.removeLayer.call(this,t),this._popupContent&&this.invoke("unbindPopup"),this.fire("layerremove",{layer:t})},bindPopup:function(t,e){return this._popupContent=t,this._popupOptions=e,this.invoke("bindPopup",t,e)},setStyle:function(t){return this.invoke("setStyle",t)},bringToFront:function(){return this.invoke("bringToFront")},bringToBack:function(){return this.invoke("bringToBack")},getBounds:function(){var t=new n.LatLngBounds;return this.eachLayer(function(e){t.extend(e instanceof n.Marker?e.getLatLng():e.getBounds())}),t},_propagateEvent:function(t){t.layer=t.target,t.target=this,this.fire(t.type,t)}}),n.featureGroup=function(t){return new n.FeatureGroup(t)},n.Path=n.Class.extend({includes:[n.Mixin.Events],statics:{CLIP_PADDING:n.Browser.mobile?Math.max(0,Math.min(.5,(1280/Math.max(t.innerWidth,t.innerHeight)-1)/2)):.5},options:{stroke:!0,color:"#0033ff",dashArray:null,weight:5,opacity:.5,fill:!1,fillColor:null,fillOpacity:.2,clickable:!0},initialize:function(t){n.setOptions(this,t)},onAdd:function(t){this._map=t,this._container||(this._initElements(),this._initEvents()),this.projectLatlngs(),this._updatePath(),this._container&&this._map._pathRoot.appendChild(this._container),this.fire("add"),t.on({viewreset:this.projectLatlngs,moveend:this._updatePath},this)},addTo:function(t){return t.addLayer(this),this},onRemove:function(t){t._pathRoot.removeChild(this._container),this.fire("remove"),this._map=null,n.Browser.vml&&(this._container=null,this._stroke=null,this._fill=null),t.off({viewreset:this.projectLatlngs,moveend:this._updatePath},this)},projectLatlngs:function(){},setStyle:function(t){return n.setOptions(this,t),this._container&&this._updateStyle(),this},redraw:function(){return this._map&&(this.projectLatlngs(),this._updatePath()),this}}),n.Map.include({_updatePathViewport:function(){var t=n.Path.CLIP_PADDING,e=this.getSize(),i=n.DomUtil.getPosition(this._mapPane),o=i.multiplyBy(-1)._subtract(e.multiplyBy(t)._round()),s=o.add(e.multiplyBy(1+2*t)._round());this._pathViewport=new n.Bounds(o,s)}}),n.Path.SVG_NS="http://www.w3.org/2000/svg",n.Browser.svg=!(!e.createElementNS||!e.createElementNS(n.Path.SVG_NS,"svg").createSVGRect),n.Path=n.Path.extend({statics:{SVG:n.Browser.svg},bringToFront:function(){var t=this._map._pathRoot,e=this._container;return e&&t.lastChild!==e&&t.appendChild(e),this},bringToBack:function(){var t=this._map._pathRoot,e=this._container,i=t.firstChild;return e&&i!==e&&t.insertBefore(e,i),this},getPathString:function(){},_createElement:function(t){return e.createElementNS(n.Path.SVG_NS,t)},_initElements:function(){this._map._initPathRoot(),this._initPath(),this._initStyle()},_initPath:function(){this._container=this._createElement("g"),this._path=this._createElement("path"),this._container.appendChild(this._path)},_initStyle:function(){this.options.stroke&&(this._path.setAttribute("stroke-linejoin","round"),this._path.setAttribute("stroke-linecap","round")),this.options.fill&&this._path.setAttribute("fill-rule","evenodd"),this._updateStyle()},_updateStyle:function(){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),this.options.dashArray?this._path.setAttribute("stroke-dasharray",this.options.dashArray):this._path.removeAttribute("stroke-dasharray")):this._path.setAttribute("stroke","none"),this.options.fill?(this._path.setAttribute("fill",this.options.fillColor||this.options.color),this._path.setAttribute("fill-opacity",this.options.fillOpacity)):this._path.setAttribute("fill","none")},_updatePath:function(){var t=this.getPathString();t||(t="M0 0"),this._path.setAttribute("d",t)},_initEvents:function(){if(this.options.clickable){(n.Browser.svg||!n.Browser.vml)&&this._path.setAttribute("class","leaflet-clickable"),n.DomEvent.on(this._container,"click",this._onMouseClick,this);for(var t=["dblclick","mousedown","mouseover","mouseout","mousemove","contextmenu"],e=0;t.length>e;e++)n.DomEvent.on(this._container,t[e],this._fireMouseEvent,this)}},_onMouseClick:function(t){this._map.dragging&&this._map.dragging.moved()||this._fireMouseEvent(t)},_fireMouseEvent:function(t){if(this.hasEventListeners(t.type)){var e=this._map,i=e.mouseEventToContainerPoint(t),o=e.containerPointToLayerPoint(i),s=e.layerPointToLatLng(o);this.fire(t.type,{latlng:s,layerPoint:o,containerPoint:i,originalEvent:t}),"contextmenu"===t.type&&n.DomEvent.preventDefault(t),"mousemove"!==t.type&&n.DomEvent.stopPropagation(t)}}}),n.Map.include({_initPathRoot:function(){this._pathRoot||(this._pathRoot=n.Path.prototype._createElement("svg"),this._panes.overlayPane.appendChild(this._pathRoot),this.options.zoomAnimation&&n.Browser.any3d?(this._pathRoot.setAttribute("class"," leaflet-zoom-animated"),this.on({zoomanim:this._animatePathZoom,zoomend:this._endPathZoom})):this._pathRoot.setAttribute("class"," leaflet-zoom-hide"),this.on("moveend",this._updateSvgViewport),this._updateSvgViewport())},_animatePathZoom:function(t){var e=this.getZoomScale(t.zoom),i=this._getCenterOffset(t.center)._multiplyBy(-e)._add(this._pathViewport.min);this._pathRoot.style[n.DomUtil.TRANSFORM]=n.DomUtil.getTranslateString(i)+" scale("+e+") ",this._pathZooming=!0},_endPathZoom:function(){this._pathZooming=!1},_updateSvgViewport:function(){if(!this._pathZooming){this._updatePathViewport();var t=this._pathViewport,e=t.min,i=t.max,o=i.x-e.x,s=i.y-e.y,a=this._pathRoot,r=this._panes.overlayPane;n.Browser.mobileWebkit&&r.removeChild(a),n.DomUtil.setPosition(a,e),a.setAttribute("width",o),a.setAttribute("height",s),a.setAttribute("viewBox",[e.x,e.y,o,s].join(" ")),n.Browser.mobileWebkit&&r.appendChild(a)}}}),n.Path.include({bindPopup:function(t,e){return(!this._popup||e)&&(this._popup=new n.Popup(e,this)),this._popup.setContent(t),this._popupHandlersAdded||(this.on("click",this._openPopup,this).on("remove",this.closePopup,this),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this._popup=null,this.off("click",this._openPopup).off("remove",this.closePopup),this._popupHandlersAdded=!1),this},openPopup:function(t){return this._popup&&(t=t||this._latlng||this._latlngs[Math.floor(this._latlngs.length/2)],this._openPopup({latlng:t})),this},closePopup:function(){return this._popup&&this._popup._close(),this},_openPopup:function(t){this._popup.setLatLng(t.latlng),this._map.openPopup(this._popup)}}),n.Browser.vml=!n.Browser.svg&&function(){try{var t=e.createElement("div");t.innerHTML='';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(n){return!1}}(),n.Path=n.Browser.svg||!n.Browser.vml?n.Path:n.Path.extend({statics:{VML:!0,CLIP_PADDING:.02},_createElement:function(){try{return e.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(t){return e.createElement("')}}catch(t){return function(t){return e.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),_initPath:function(){var t=this._container=this._createElement("shape");n.DomUtil.addClass(t,"leaflet-vml-shape"),this.options.clickable&&n.DomUtil.addClass(t,"leaflet-clickable"),t.coordsize="1 1",this._path=this._createElement("path"),t.appendChild(this._path),this._map._pathRoot.appendChild(t)},_initStyle:function(){this._updateStyle()},_updateStyle:function(){var t=this._stroke,e=this._fill,i=this.options,n=this._container;n.stroked=i.stroke,n.filled=i.fill,i.stroke?(t||(t=this._stroke=this._createElement("stroke"),t.endcap="round",n.appendChild(t)),t.weight=i.weight+"px",t.color=i.color,t.opacity=i.opacity,t.dashStyle=i.dashArray?i.dashArray instanceof Array?i.dashArray.join(" "):i.dashArray.replace(/ *, */g," "):""):t&&(n.removeChild(t),this._stroke=null),i.fill?(e||(e=this._fill=this._createElement("fill"),n.appendChild(e)),e.color=i.fillColor||i.color,e.opacity=i.fillOpacity):e&&(n.removeChild(e),this._fill=null)},_updatePath:function(){var t=this._container.style;t.display="none",this._path.v=this.getPathString()+" ",t.display=""}}),n.Map.include(n.Browser.svg||!n.Browser.vml?{}:{_initPathRoot:function(){if(!this._pathRoot){var t=this._pathRoot=e.createElement("div");t.className="leaflet-vml-container",this._panes.overlayPane.appendChild(t),this.on("moveend",this._updatePathViewport),this._updatePathViewport()}}}),n.Browser.canvas=function(){return!!e.createElement("canvas").getContext}(),n.Path=n.Path.SVG&&!t.L_PREFER_CANVAS||!n.Browser.canvas?n.Path:n.Path.extend({statics:{CANVAS:!0,SVG:!1},redraw:function(){return this._map&&(this.projectLatlngs(),this._requestUpdate()),this},setStyle:function(t){return n.setOptions(this,t),this._map&&(this._updateStyle(),this._requestUpdate()),this},onRemove:function(t){t.off("viewreset",this.projectLatlngs,this).off("moveend",this._updatePath,this),this.options.clickable&&this._map.off("click",this._onClick,this),this._requestUpdate(),this._map=null},_requestUpdate:function(){this._map&&!n.Path._updateRequest&&(n.Path._updateRequest=n.Util.requestAnimFrame(this._fireMapMoveEnd,this._map))},_fireMapMoveEnd:function(){n.Path._updateRequest=null,this.fire("moveend")},_initElements:function(){this._map._initPathRoot(),this._ctx=this._map._canvasCtx},_updateStyle:function(){var t=this.options;t.stroke&&(this._ctx.lineWidth=t.weight,this._ctx.strokeStyle=t.color),t.fill&&(this._ctx.fillStyle=t.fillColor||t.color)},_drawPath:function(){var t,e,i,o,s,a;for(this._ctx.beginPath(),t=0,i=this._parts.length;i>t;t++){for(e=0,o=this._parts[t].length;o>e;e++)s=this._parts[t][e],a=(0===e?"move":"line")+"To",this._ctx[a](s.x,s.y);this instanceof n.Polygon&&this._ctx.closePath()}},_checkIfEmpty:function(){return!this._parts.length},_updatePath:function(){if(!this._checkIfEmpty()){var t=this._ctx,e=this.options;this._drawPath(),t.save(),this._updateStyle(),e.fill&&(t.globalAlpha=e.fillOpacity,t.fill()),e.stroke&&(t.globalAlpha=e.opacity,t.stroke()),t.restore()}},_initEvents:function(){this.options.clickable&&this._map.on("click",this._onClick,this)},_onClick:function(t){this._containsPoint(t.layerPoint)&&this.fire("click",{latlng:t.latlng,layerPoint:t.layerPoint,containerPoint:t.containerPoint,originalEvent:t})}}),n.Map.include(n.Path.SVG&&!t.L_PREFER_CANVAS||!n.Browser.canvas?{}:{_initPathRoot:function(){var t,i=this._pathRoot;i||(i=this._pathRoot=e.createElement("canvas"),i.style.position="absolute",t=this._canvasCtx=i.getContext("2d"),t.lineCap="round",t.lineJoin="round",this._panes.overlayPane.appendChild(i),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(){if(!this._pathZooming){this._updatePathViewport();var t=this._pathViewport,e=t.min,i=t.max.subtract(e),o=this._pathRoot;n.DomUtil.setPosition(o,e),o.width=i.x,o.height=i.y,o.getContext("2d").translate(-e.x,-e.y)}}}),n.LineUtil={simplify:function(t,e){if(!e||!t.length)return t.slice();var i=e*e;return t=this._reducePoints(t,i),t=this._simplifyDP(t,i)},pointToSegmentDistance:function(t,e,i){return Math.sqrt(this._sqClosestPointOnSegment(t,e,i,!0))},closestPointOnSegment:function(t,e,i){return this._sqClosestPointOnSegment(t,e,i)},_simplifyDP:function(t,e){var n=t.length,o=typeof Uint8Array!=i+""?Uint8Array:Array,s=new o(n);s[0]=s[n-1]=1,this._simplifyDPStep(t,s,e,0,n-1);var a,r=[];for(a=0;n>a;a++)s[a]&&r.push(t[a]);return r},_simplifyDPStep:function(t,e,i,n,o){var s,a,r,h=0;for(a=n+1;o-1>=a;a++)r=this._sqClosestPointOnSegment(t[a],t[n],t[o],!0),r>h&&(s=a,h=r);h>i&&(e[s]=1,this._simplifyDPStep(t,e,i,n,s),this._simplifyDPStep(t,e,i,s,o))},_reducePoints:function(t,e){for(var i=[t[0]],n=1,o=0,s=t.length;s>n;n++)this._sqDist(t[n],t[o])>e&&(i.push(t[n]),o=n);return s-1>o&&i.push(t[s-1]),i},clipSegment:function(t,e,i,n){var o,s,a,r=n?this._lastCode:this._getBitCode(t,i),h=this._getBitCode(e,i);for(this._lastCode=h;;){if(!(r|h))return[t,e];if(r&h)return!1;o=r||h,s=this._getEdgeIntersection(t,e,o,i),a=this._getBitCode(s,i),o===r?(t=s,r=a):(e=s,h=a)}},_getEdgeIntersection:function(t,e,o,s){var a=e.x-t.x,r=e.y-t.y,h=s.min,l=s.max;return 8&o?new n.Point(t.x+a*(l.y-t.y)/r,l.y):4&o?new n.Point(t.x+a*(h.y-t.y)/r,h.y):2&o?new n.Point(l.x,t.y+r*(l.x-t.x)/a):1&o?new n.Point(h.x,t.y+r*(h.x-t.x)/a):i},_getBitCode:function(t,e){var i=0;return t.xe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n},_sqClosestPointOnSegment:function(t,e,i,o){var s,a=e.x,r=e.y,h=i.x-a,l=i.y-r,u=h*h+l*l;return u>0&&(s=((t.x-a)*h+(t.y-r)*l)/u,s>1?(a=i.x,r=i.y):s>0&&(a+=h*s,r+=l*s)),h=t.x-a,l=t.y-r,o?h*h+l*l:new n.Point(a,r)}},n.Polyline=n.Path.extend({initialize:function(t,e){n.Path.prototype.initialize.call(this,e),this._latlngs=this._convertLatLngs(t)},options:{smoothFactor:1,noClip:!1},projectLatlngs:function(){this._originalPoints=[];for(var t=0,e=this._latlngs.length;e>t;t++)this._originalPoints[t]=this._map.latLngToLayerPoint(this._latlngs[t])},getPathString:function(){for(var t=0,e=this._parts.length,i="";e>t;t++)i+=this._getPathPartStr(this._parts[t]);return i},getLatLngs:function(){return this._latlngs},setLatLngs:function(t){return this._latlngs=this._convertLatLngs(t),this.redraw()},addLatLng:function(t){return this._latlngs.push(n.latLng(t)),this.redraw()},spliceLatLngs:function(){var t=[].splice.apply(this._latlngs,arguments);return this._convertLatLngs(this._latlngs),this.redraw(),t},closestLayerPoint:function(t){for(var e,i,o=1/0,s=this._parts,a=null,r=0,h=s.length;h>r;r++)for(var l=s[r],u=1,c=l.length;c>u;u++){e=l[u-1],i=l[u]; +var _=n.LineUtil._sqClosestPointOnSegment(t,e,i,!0);o>_&&(o=_,a=n.LineUtil._sqClosestPointOnSegment(t,e,i))}return a&&(a.distance=Math.sqrt(o)),a},getBounds:function(){var t,e,i=new n.LatLngBounds,o=this.getLatLngs();for(t=0,e=o.length;e>t;t++)i.extend(o[t]);return i},_convertLatLngs:function(t){var e,i;for(e=0,i=t.length;i>e;e++){if(n.Util.isArray(t[e])&&"number"!=typeof t[e][0])return;t[e]=n.latLng(t[e])}return t},_initEvents:function(){n.Path.prototype._initEvents.call(this)},_getPathPartStr:function(t){for(var e,i=n.Path.VML,o=0,s=t.length,a="";s>o;o++)e=t[o],i&&e._round(),a+=(o?"L":"M")+e.x+" "+e.y;return a},_clipPoints:function(){var t,e,o,s=this._originalPoints,a=s.length;if(this.options.noClip)return this._parts=[s],i;this._parts=[];var r=this._parts,h=this._map._pathViewport,l=n.LineUtil;for(t=0,e=0;a-1>t;t++)o=l.clipSegment(s[t],s[t+1],h,t),o&&(r[e]=r[e]||[],r[e].push(o[0]),(o[1]!==s[t+1]||t===a-2)&&(r[e].push(o[1]),e++))},_simplifyPoints:function(){for(var t=this._parts,e=n.LineUtil,i=0,o=t.length;o>i;i++)t[i]=e.simplify(t[i],this.options.smoothFactor)},_updatePath:function(){this._map&&(this._clipPoints(),this._simplifyPoints(),n.Path.prototype._updatePath.call(this))}}),n.polyline=function(t,e){return new n.Polyline(t,e)},n.PolyUtil={},n.PolyUtil.clipPolygon=function(t,e){var i,o,s,a,r,h,l,u,c,_=[1,4,2,8],d=n.LineUtil;for(o=0,l=t.length;l>o;o++)t[o]._code=d._getBitCode(t[o],e);for(a=0;4>a;a++){for(u=_[a],i=[],o=0,l=t.length,s=l-1;l>o;s=o++)r=t[o],h=t[s],r._code&u?h._code&u||(c=d._getEdgeIntersection(h,r,u,e),c._code=d._getBitCode(c,e),i.push(c)):(h._code&u&&(c=d._getEdgeIntersection(h,r,u,e),c._code=d._getBitCode(c,e),i.push(c)),i.push(r));t=i}return t},n.Polygon=n.Polyline.extend({options:{fill:!0},initialize:function(t,e){n.Polyline.prototype.initialize.call(this,t,e),t&&n.Util.isArray(t[0])&&"number"!=typeof t[0][0]&&(this._latlngs=this._convertLatLngs(t[0]),this._holes=t.slice(1))},projectLatlngs:function(){if(n.Polyline.prototype.projectLatlngs.call(this),this._holePoints=[],this._holes){var t,e,i,o;for(t=0,i=this._holes.length;i>t;t++)for(this._holePoints[t]=[],e=0,o=this._holes[t].length;o>e;e++)this._holePoints[t][e]=this._map.latLngToLayerPoint(this._holes[t][e])}},_clipPoints:function(){var t=this._originalPoints,e=[];if(this._parts=[t].concat(this._holePoints),!this.options.noClip){for(var i=0,o=this._parts.length;o>i;i++){var s=n.PolyUtil.clipPolygon(this._parts[i],this._map._pathViewport);s.length&&e.push(s)}this._parts=e}},_getPathPartStr:function(t){var e=n.Polyline.prototype._getPathPartStr.call(this,t);return e+(n.Browser.svg?"z":"x")}}),n.polygon=function(t,e){return new n.Polygon(t,e)},function(){function t(t){return n.FeatureGroup.extend({initialize:function(t,e){this._layers={},this._options=e,this.setLatLngs(t)},setLatLngs:function(e){var i=0,n=e.length;for(this.eachLayer(function(t){n>i?t.setLatLngs(e[i++]):this.removeLayer(t)},this);n>i;)this.addLayer(new t(e[i++],this._options));return this}})}n.MultiPolyline=t(n.Polyline),n.MultiPolygon=t(n.Polygon),n.multiPolyline=function(t,e){return new n.MultiPolyline(t,e)},n.multiPolygon=function(t,e){return new n.MultiPolygon(t,e)}}(),n.Rectangle=n.Polygon.extend({initialize:function(t,e){n.Polygon.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=n.latLngBounds(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}}),n.rectangle=function(t,e){return new n.Rectangle(t,e)},n.Circle=n.Path.extend({initialize:function(t,e,i){n.Path.prototype.initialize.call(this,i),this._latlng=n.latLng(t),this._mRadius=e},options:{fill:!0},setLatLng:function(t){return this._latlng=n.latLng(t),this.redraw()},setRadius:function(t){return this._mRadius=t,this.redraw()},projectLatlngs:function(){var t=this._getLngRadius(),e=new n.LatLng(this._latlng.lat,this._latlng.lng-t),i=this._map.latLngToLayerPoint(e);this._point=this._map.latLngToLayerPoint(this._latlng),this._radius=Math.max(Math.round(this._point.x-i.x),1)},getBounds:function(){var t=this._getLngRadius(),e=360*(this._mRadius/40075017),i=this._latlng,o=new n.LatLng(i.lat-e,i.lng-t),s=new n.LatLng(i.lat+e,i.lng+t);return new n.LatLngBounds(o,s)},getLatLng:function(){return this._latlng},getPathString:function(){var t=this._point,e=this._radius;return this._checkIfEmpty()?"":n.Browser.svg?"M"+t.x+","+(t.y-e)+"A"+e+","+e+",0,1,1,"+(t.x-.1)+","+(t.y-e)+" z":(t._round(),e=Math.round(e),"AL "+t.x+","+t.y+" "+e+","+e+" 0,"+23592600)},getRadius:function(){return this._mRadius},_getLatRadius:function(){return 360*(this._mRadius/40075017)},_getLngRadius:function(){return this._getLatRadius()/Math.cos(n.LatLng.DEG_TO_RAD*this._latlng.lat)},_checkIfEmpty:function(){if(!this._map)return!1;var t=this._map._pathViewport,e=this._radius,i=this._point;return i.x-e>t.max.x||i.y-e>t.max.y||i.x+ei;i++)for(l=this._parts[i],o=0,r=l.length,s=r-1;r>o;s=o++)if((e||0!==o)&&(h=n.LineUtil.pointToSegmentDistance(t,l[s],l[o]),u>=h))return!0;return!1}}:{}),n.Polygon.include(n.Path.CANVAS?{_containsPoint:function(t){var e,i,o,s,a,r,h,l,u=!1;if(n.Polyline.prototype._containsPoint.call(this,t,!0))return!0;for(s=0,h=this._parts.length;h>s;s++)for(e=this._parts[s],a=0,l=e.length,r=l-1;l>a;r=a++)i=e[a],o=e[r],i.y>t.y!=o.y>t.y&&t.x<(o.x-i.x)*(t.y-i.y)/(o.y-i.y)+i.x&&(u=!u);return u}}:{}),n.Circle.include(n.Path.CANVAS?{_drawPath:function(){var t=this._point;this._ctx.beginPath(),this._ctx.arc(t.x,t.y,this._radius,0,2*Math.PI,!1)},_containsPoint:function(t){var e=this._point,i=this.options.stroke?this.options.weight/2:0;return t.distanceTo(e)<=this._radius+i}}:{}),n.GeoJSON=n.FeatureGroup.extend({initialize:function(t,e){n.setOptions(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,o=n.Util.isArray(t)?t:t.features;if(o){for(e=0,i=o.length;i>e;e++)(o[e].geometries||o[e].geometry||o[e].features)&&this.addData(o[e]);return this}var s=this.options;if(!s.filter||s.filter(t)){var a=n.GeoJSON.geometryToLayer(t,s.pointToLayer);return a.feature=t,a.defaultOptions=a.options,this.resetStyle(a),s.onEachFeature&&s.onEachFeature(t,a),this.addLayer(a)}},resetStyle:function(t){var e=this.options.style;e&&(n.Util.extend(t.options,t.defaultOptions),this._setLayerStyle(t,e))},setStyle:function(t){this.eachLayer(function(e){this._setLayerStyle(e,t)},this)},_setLayerStyle:function(t,e){"function"==typeof e&&(e=e(t.feature)),t.setStyle&&t.setStyle(e)}}),n.extend(n.GeoJSON,{geometryToLayer:function(t,e){var i,o,s,a,r,h="Feature"===t.type?t.geometry:t,l=h.coordinates,u=[];switch(h.type){case"Point":return i=this.coordsToLatLng(l),e?e(t,i):new n.Marker(i);case"MultiPoint":for(s=0,a=l.length;a>s;s++)i=this.coordsToLatLng(l[s]),r=e?e(t,i):new n.Marker(i),u.push(r);return new n.FeatureGroup(u);case"LineString":return o=this.coordsToLatLngs(l),new n.Polyline(o);case"Polygon":return o=this.coordsToLatLngs(l,1),new n.Polygon(o);case"MultiLineString":return o=this.coordsToLatLngs(l,1),new n.MultiPolyline(o);case"MultiPolygon":return o=this.coordsToLatLngs(l,2),new n.MultiPolygon(o);case"GeometryCollection":for(s=0,a=h.geometries.length;a>s;s++)r=this.geometryToLayer({geometry:h.geometries[s],type:"Feature",properties:t.properties},e),u.push(r);return new n.FeatureGroup(u);default:throw Error("Invalid GeoJSON object.")}},coordsToLatLng:function(t,e){var i=parseFloat(t[e?0:1]),o=parseFloat(t[e?1:0]);return new n.LatLng(i,o)},coordsToLatLngs:function(t,e,i){var n,o,s,a=[];for(o=0,s=t.length;s>o;o++)n=e?this.coordsToLatLngs(t[o],e-1,i):this.coordsToLatLng(t[o],i),a.push(n);return a}}),n.geoJson=function(t,e){return new n.GeoJSON(t,e)},n.DomEvent={addListener:function(t,e,o,s){var a,r,h,l=n.stamp(o),u="_leaflet_"+e+l;return t[u]?this:(a=function(e){return o.call(s||t,e||n.DomEvent._getEvent())},n.Browser.msTouch&&0===e.indexOf("touch")?this.addMsTouchListener(t,e,a,l):(n.Browser.touch&&"dblclick"===e&&this.addDoubleTapListener&&this.addDoubleTapListener(t,a,l),"addEventListener"in t?"mousewheel"===e?(t.addEventListener("DOMMouseScroll",a,!1),t.addEventListener(e,a,!1)):"mouseenter"===e||"mouseleave"===e?(r=a,h="mouseenter"===e?"mouseover":"mouseout",a=function(e){return n.DomEvent._checkMouse(t,e)?r(e):i},t.addEventListener(h,a,!1)):t.addEventListener(e,a,!1):"attachEvent"in t&&t.attachEvent("on"+e,a),t[u]=a,this))},removeListener:function(t,e,i){var o=n.stamp(i),s="_leaflet_"+e+o,a=t[s];if(a)return n.Browser.msTouch&&0===e.indexOf("touch")?this.removeMsTouchListener(t,e,o):n.Browser.touch&&"dblclick"===e&&this.removeDoubleTapListener?this.removeDoubleTapListener(t,o):"removeEventListener"in t?"mousewheel"===e?(t.removeEventListener("DOMMouseScroll",a,!1),t.removeEventListener(e,a,!1)):"mouseenter"===e||"mouseleave"===e?t.removeEventListener("mouseenter"===e?"mouseover":"mouseout",a,!1):t.removeEventListener(e,a,!1):"detachEvent"in t&&t.detachEvent("on"+e,a),t[s]=null,this},stopPropagation:function(t){return t.stopPropagation?t.stopPropagation():t.cancelBubble=!0,this},disableClickPropagation:function(t){for(var e=n.DomEvent.stopPropagation,i=n.Draggable.START.length-1;i>=0;i--)n.DomEvent.addListener(t,n.Draggable.START[i],e);return n.DomEvent.addListener(t,"click",e).addListener(t,"dblclick",e)},preventDefault:function(t){return t.preventDefault?t.preventDefault():t.returnValue=!1,this},stop:function(t){return n.DomEvent.preventDefault(t).stopPropagation(t)},getMousePosition:function(t,i){var o=e.body,s=e.documentElement,a=t.pageX?t.pageX:t.clientX+o.scrollLeft+s.scrollLeft,r=t.pageY?t.pageY:t.clientY+o.scrollTop+s.scrollTop,h=new n.Point(a,r);return i?h._subtract(n.DomUtil.getViewportOffset(i)):h},getWheelDelta:function(t){var e=0;return t.wheelDelta&&(e=t.wheelDelta/120),t.detail&&(e=-t.detail/3),e},_checkMouse:function(t,e){var i=e.relatedTarget;if(!i)return!0;try{for(;i&&i!==t;)i=i.parentNode}catch(n){return!1}return i!==t},_getEvent:function(){var e=t.event;if(!e)for(var i=arguments.callee.caller;i&&(e=i.arguments[0],!e||t.Event!==e.constructor);)i=i.caller;return e}},n.DomEvent.on=n.DomEvent.addListener,n.DomEvent.off=n.DomEvent.removeListener,n.Draggable=n.Class.extend({includes:n.Mixin.Events,statics:{START:n.Browser.touch?["touchstart","mousedown"]:["mousedown"],END:{mousedown:"mouseup",touchstart:"touchend",MSPointerDown:"touchend"},MOVE:{mousedown:"mousemove",touchstart:"touchmove",MSPointerDown:"touchmove"},TAP_TOLERANCE:15},initialize:function(t,e,i){this._element=t,this._dragStartTarget=e||t,this._longPress=i&&!n.Browser.msTouch},enable:function(){if(!this._enabled){for(var t=n.Draggable.START.length-1;t>=0;t--)n.DomEvent.on(this._dragStartTarget,n.Draggable.START[t],this._onDown,this);this._enabled=!0}},disable:function(){if(this._enabled){for(var t=n.Draggable.START.length-1;t>=0;t--)n.DomEvent.off(this._dragStartTarget,n.Draggable.START[t],this._onDown,this);this._enabled=!1,this._moved=!1}},_onDown:function(t){if(!(!n.Browser.touch&&t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(n.DomEvent.preventDefault(t),n.DomEvent.stopPropagation(t),n.Draggable._disabled))){if(this._simulateClick=!0,t.touches&&t.touches.length>1)return this._simulateClick=!1,clearTimeout(this._longPressTimeout),i;var o=t.touches&&1===t.touches.length?t.touches[0]:t,s=o.target;n.Browser.touch&&"a"===s.tagName.toLowerCase()&&n.DomUtil.addClass(s,"leaflet-active"),this._moved=!1,this._moving||(this._startPoint=new n.Point(o.clientX,o.clientY),this._startPos=this._newPos=n.DomUtil.getPosition(this._element),t.touches&&1===t.touches.length&&n.Browser.touch&&this._longPress&&(this._longPressTimeout=setTimeout(n.bind(function(){var t=this._newPos&&this._newPos.distanceTo(this._startPos)||0;n.Draggable.TAP_TOLERANCE>t&&(this._simulateClick=!1,this._onUp(),this._simulateEvent("contextmenu",o))},this),1e3)),n.DomEvent.on(e,n.Draggable.MOVE[t.type],this._onMove,this),n.DomEvent.on(e,n.Draggable.END[t.type],this._onUp,this))}},_onMove:function(t){if(!(t.touches&&t.touches.length>1)){var e=t.touches&&1===t.touches.length?t.touches[0]:t,i=new n.Point(e.clientX,e.clientY),o=i.subtract(this._startPoint);(o.x||o.y)&&(n.DomEvent.preventDefault(t),this._moved||(this.fire("dragstart"),this._moved=!0,this._startPos=n.DomUtil.getPosition(this._element).subtract(o),n.Browser.touch||(n.DomUtil.disableTextSelection(),this._setMovingCursor())),this._newPos=this._startPos.add(o),this._moving=!0,n.Util.cancelAnimFrame(this._animRequest),this._animRequest=n.Util.requestAnimFrame(this._updatePosition,this,!0,this._dragStartTarget))}},_updatePosition:function(){this.fire("predrag"),n.DomUtil.setPosition(this._element,this._newPos),this.fire("drag")},_onUp:function(t){var i;if(clearTimeout(this._longPressTimeout),this._simulateClick&&t.changedTouches){var o=t.changedTouches[0],s=o.target,a=this._newPos&&this._newPos.distanceTo(this._startPos)||0;"a"===s.tagName.toLowerCase()&&n.DomUtil.removeClass(s,"leaflet-active"),n.Draggable.TAP_TOLERANCE>a&&(i=o)}n.Browser.touch||(n.DomUtil.enableTextSelection(),this._restoreCursor());for(var r in n.Draggable.MOVE)n.Draggable.MOVE.hasOwnProperty(r)&&(n.DomEvent.off(e,n.Draggable.MOVE[r],this._onMove),n.DomEvent.off(e,n.Draggable.END[r],this._onUp));this._moved&&(n.Util.cancelAnimFrame(this._animRequest),this.fire("dragend")),this._moving=!1,i&&(this._moved=!1,this._simulateEvent("click",i))},_setMovingCursor:function(){n.DomUtil.addClass(e.body,"leaflet-dragging")},_restoreCursor:function(){n.DomUtil.removeClass(e.body,"leaflet-dragging")},_simulateEvent:function(i,n){var o=e.createEvent("MouseEvents");o.initMouseEvent(i,!0,!0,t,1,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),n.target.dispatchEvent(o)}}),n.Handler=n.Class.extend({initialize:function(t){this._map=t},enable:function(){this._enabled||(this._enabled=!0,this.addHooks())},disable:function(){this._enabled&&(this._enabled=!1,this.removeHooks())},enabled:function(){return!!this._enabled}}),n.Map.mergeOptions({dragging:!0,inertia:!n.Browser.android23,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,inertiaThreshold:n.Browser.touch?32:18,easeLinearity:.25,longPress:!0,worldCopyJump:!1}),n.Map.Drag=n.Handler.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new n.Draggable(t._mapPane,t._container,t.options.longPress),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDrag,this),t.on("viewreset",this._onViewReset,this))}this._draggable.enable()},removeHooks:function(){this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){var t=this._map;t._panAnim&&t._panAnim.stop(),t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(){if(this._map.options.inertia){var t=this._lastTime=+new Date,e=this._lastPos=this._draggable._newPos;this._positions.push(e),this._times.push(t),t-this._times[0]>200&&(this._positions.shift(),this._times.shift())}this._map.fire("move").fire("drag")},_onViewReset:function(){var t=this._map.getSize()._divideBy(2),e=this._map.latLngToLayerPoint(new n.LatLng(0,0));this._initialWorldOffset=e.subtract(t).x,this._worldWidth=this._map.project(new n.LatLng(0,180)).x},_onPreDrag:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,s=(n+e+i)%t-e-i,a=Math.abs(o+i)e.inertiaThreshold||!this._positions[0];if(o)t.fire("moveend");else{var s=this._lastPos.subtract(this._positions[0]),a=(this._lastTime+i-this._times[0])/1e3,r=e.easeLinearity,h=s.multiplyBy(r/a),l=h.distanceTo(new n.Point(0,0)),u=Math.min(e.inertiaMaxSpeed,l),c=h.multiplyBy(u/l),_=u/(e.inertiaDeceleration*r),d=c.multiplyBy(-_/2).round();n.Util.requestAnimFrame(function(){t.panBy(d,_,r)})}t.fire("dragend"),e.maxBounds&&n.Util.requestAnimFrame(this._panInsideMaxBounds,t,!0,t._container)},_panInsideMaxBounds:function(){this.panInsideBounds(this.options.maxBounds)}}),n.Map.addInitHook("addHandler","dragging",n.Map.Drag),n.Map.mergeOptions({doubleClickZoom:!0}),n.Map.DoubleClickZoom=n.Handler.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick)},_onDoubleClick:function(t){this.setView(t.latlng,this._zoom+1)}}),n.Map.addInitHook("addHandler","doubleClickZoom",n.Map.DoubleClickZoom),n.Map.mergeOptions({scrollWheelZoom:!0}),n.Map.ScrollWheelZoom=n.Handler.extend({addHooks:function(){n.DomEvent.on(this._map._container,"mousewheel",this._onWheelScroll,this),this._delta=0},removeHooks:function(){n.DomEvent.off(this._map._container,"mousewheel",this._onWheelScroll)},_onWheelScroll:function(t){var e=n.DomEvent.getWheelDelta(t);this._delta+=e,this._lastMousePos=this._map.mouseEventToContainerPoint(t),this._startTime||(this._startTime=+new Date);var i=Math.max(40-(+new Date-this._startTime),0);clearTimeout(this._timer),this._timer=setTimeout(n.bind(this._performZoom,this),i),n.DomEvent.preventDefault(t),n.DomEvent.stopPropagation(t)},_performZoom:function(){var t=this._map,e=this._delta,i=t.getZoom();if(e=e>0?Math.ceil(e):Math.round(e),e=Math.max(Math.min(e,4),-4),e=t._limitZoom(i+e)-i,this._delta=0,this._startTime=null,e){var n=i+e,o=this._getCenterForScrollWheelZoom(n);t.setView(o,n)}},_getCenterForScrollWheelZoom:function(t){var e=this._map,i=e.getZoomScale(t),n=e.getSize()._divideBy(2),o=this._lastMousePos._subtract(n)._multiplyBy(1-1/i),s=e._getTopLeftPoint()._add(n)._add(o);return e.unproject(s)}}),n.Map.addInitHook("addHandler","scrollWheelZoom",n.Map.ScrollWheelZoom),n.extend(n.DomEvent,{_touchstart:n.Browser.msTouch?"MSPointerDown":"touchstart",_touchend:n.Browser.msTouch?"MSPointerUp":"touchend",addDoubleTapListener:function(t,i,o){function s(t){var e;if(n.Browser.msTouch?(p.push(t.pointerId),e=p.length):e=t.touches.length,!(e>1)){var i=Date.now(),o=i-(r||i);h=t.touches?t.touches[0]:t,l=o>0&&u>=o,r=i}}function a(t){if(n.Browser.msTouch){var e=p.indexOf(t.pointerId);if(-1===e)return;p.splice(e,1)}if(l){if(n.Browser.msTouch){var o,s={};for(var a in h)o=h[a],s[a]="function"==typeof o?o.bind(h):o;h=s}h.type="dblclick",i(h),r=null}}var r,h,l=!1,u=250,c="_leaflet_",_=this._touchstart,d=this._touchend,p=[];t[c+_+o]=s,t[c+d+o]=a;var m=n.Browser.msTouch?e.documentElement:t;return t.addEventListener(_,s,!1),m.addEventListener(d,a,!1),n.Browser.msTouch&&m.addEventListener("MSPointerCancel",a,!1),this},removeDoubleTapListener:function(t,i){var o="_leaflet_";return t.removeEventListener(this._touchstart,t[o+this._touchstart+i],!1),(n.Browser.msTouch?e.documentElement:t).removeEventListener(this._touchend,t[o+this._touchend+i],!1),n.Browser.msTouch&&e.documentElement.removeEventListener("MSPointerCancel",t[o+this._touchend+i],!1),this}}),n.extend(n.DomEvent,{_msTouches:[],_msDocumentListener:!1,addMsTouchListener:function(t,e,i,n){switch(e){case"touchstart":return this.addMsTouchListenerStart(t,e,i,n);case"touchend":return this.addMsTouchListenerEnd(t,e,i,n);case"touchmove":return this.addMsTouchListenerMove(t,e,i,n);default:throw"Unknown touch event type"}},addMsTouchListenerStart:function(t,i,n,o){var s="_leaflet_",a=this._msTouches,r=function(t){for(var e=!1,i=0;a.length>i;i++)if(a[i].pointerId===t.pointerId){e=!0;break}e||a.push(t),t.touches=a.slice(),t.changedTouches=[t],n(t)};if(t[s+"touchstart"+o]=r,t.addEventListener("MSPointerDown",r,!1),!this._msDocumentListener){var h=function(t){for(var e=0;a.length>e;e++)if(a[e].pointerId===t.pointerId){a.splice(e,1);break}};e.documentElement.addEventListener("MSPointerUp",h,!1),e.documentElement.addEventListener("MSPointerCancel",h,!1),this._msDocumentListener=!0}return this},addMsTouchListenerMove:function(t,e,i,n){function o(t){if(t.pointerType!==t.MSPOINTER_TYPE_MOUSE||0!==t.buttons){for(var e=0;a.length>e;e++)if(a[e].pointerId===t.pointerId){a[e]=t;break}t.touches=a.slice(),t.changedTouches=[t],i(t)}}var s="_leaflet_",a=this._msTouches;return t[s+"touchmove"+n]=o,t.addEventListener("MSPointerMove",o,!1),this},addMsTouchListenerEnd:function(t,e,i,n){var o="_leaflet_",s=this._msTouches,a=function(t){for(var e=0;s.length>e;e++)if(s[e].pointerId===t.pointerId){s.splice(e,1);break}t.touches=s.slice(),t.changedTouches=[t],i(t)};return t[o+"touchend"+n]=a,t.addEventListener("MSPointerUp",a,!1),t.addEventListener("MSPointerCancel",a,!1),this},removeMsTouchListener:function(t,e,i){var n="_leaflet_",o=t[n+e+i];switch(e){case"touchstart":t.removeEventListener("MSPointerDown",o,!1);break;case"touchmove":t.removeEventListener("MSPointerMove",o,!1);break;case"touchend":t.removeEventListener("MSPointerUp",o,!1),t.removeEventListener("MSPointerCancel",o,!1)}return this}}),n.Map.mergeOptions({touchZoom:n.Browser.touch&&!n.Browser.android23}),n.Map.TouchZoom=n.Handler.extend({addHooks:function(){n.DomEvent.on(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){n.DomEvent.off(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var o=i.mouseEventToLayerPoint(t.touches[0]),s=i.mouseEventToLayerPoint(t.touches[1]),a=i._getCenterLayerPoint();this._startCenter=o.add(s)._divideBy(2),this._startDist=o.distanceTo(s),this._moved=!1,this._zooming=!0,this._centerOffset=a.subtract(this._startCenter),i._panAnim&&i._panAnim.stop(),n.DomEvent.on(e,"touchmove",this._onTouchMove,this).on(e,"touchend",this._onTouchEnd,this),n.DomEvent.preventDefault(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length){var e=this._map,i=e.mouseEventToLayerPoint(t.touches[0]),o=e.mouseEventToLayerPoint(t.touches[1]);this._scale=i.distanceTo(o)/this._startDist,this._delta=i._add(o)._divideBy(2)._subtract(this._startCenter),1!==this._scale&&(this._moved||(n.DomUtil.addClass(e._mapPane,"leaflet-zoom-anim leaflet-touching"),e.fire("movestart").fire("zoomstart")._prepareTileBg(),this._moved=!0),n.Util.cancelAnimFrame(this._animRequest),this._animRequest=n.Util.requestAnimFrame(this._updateOnMove,this,!0,this._map._container),n.DomEvent.preventDefault(t))}},_updateOnMove:function(){var t=this._map,e=this._getScaleOrigin(),i=t.layerPointToLatLng(e);t.fire("zoomanim",{center:i,zoom:t.getScaleZoom(this._scale)}),t._tileBg.style[n.DomUtil.TRANSFORM]=n.DomUtil.getTranslateString(this._delta)+" "+n.DomUtil.getScaleString(this._scale,this._startCenter)},_onTouchEnd:function(){if(this._moved&&this._zooming){var t=this._map;this._zooming=!1,n.DomUtil.removeClass(t._mapPane,"leaflet-touching"),n.DomEvent.off(e,"touchmove",this._onTouchMove).off(e,"touchend",this._onTouchEnd);var i=this._getScaleOrigin(),o=t.layerPointToLatLng(i),s=t.getZoom(),a=t.getScaleZoom(this._scale)-s,r=a>0?Math.ceil(a):Math.floor(a),h=t._limitZoom(s+r);t.fire("zoomanim",{center:o,zoom:h}),t._runAnimation(o,h,t.getZoomScale(h)/this._scale,i,!0)}},_getScaleOrigin:function(){var t=this._centerOffset.subtract(this._delta).divideBy(this._scale);return this._startCenter.add(t)}}),n.Map.addInitHook("addHandler","touchZoom",n.Map.TouchZoom),n.Map.mergeOptions({boxZoom:!0}),n.Map.BoxZoom=n.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane},addHooks:function(){n.DomEvent.on(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){n.DomEvent.off(this._container,"mousedown",this._onMouseDown)},_onMouseDown:function(t){return!t.shiftKey||1!==t.which&&1!==t.button?!1:(n.DomUtil.disableTextSelection(),this._startLayerPoint=this._map.mouseEventToLayerPoint(t),this._box=n.DomUtil.create("div","leaflet-zoom-box",this._pane),n.DomUtil.setPosition(this._box,this._startLayerPoint),this._container.style.cursor="crosshair",n.DomEvent.on(e,"mousemove",this._onMouseMove,this).on(e,"mouseup",this._onMouseUp,this).preventDefault(t),this._map.fire("boxzoomstart"),i)},_onMouseMove:function(t){var e=this._startLayerPoint,i=this._box,o=this._map.mouseEventToLayerPoint(t),s=o.subtract(e),a=new n.Point(Math.min(o.x,e.x),Math.min(o.y,e.y));n.DomUtil.setPosition(i,a),i.style.width=Math.max(0,Math.abs(s.x)-4)+"px",i.style.height=Math.max(0,Math.abs(s.y)-4)+"px"},_onMouseUp:function(t){this._pane.removeChild(this._box),this._container.style.cursor="",n.DomUtil.enableTextSelection(),n.DomEvent.off(e,"mousemove",this._onMouseMove).off(e,"mouseup",this._onMouseUp);var i=this._map,o=i.mouseEventToLayerPoint(t);if(!this._startLayerPoint.equals(o)){var s=new n.LatLngBounds(i.layerPointToLatLng(this._startLayerPoint),i.layerPointToLatLng(o));i.fitBounds(s),i.fire("boxzoomend",{boxZoomBounds:s})}}}),n.Map.addInitHook("addHandler","boxZoom",n.Map.BoxZoom),n.Map.mergeOptions({keyboard:!0,keyboardPanOffset:80,keyboardZoomOffset:1}),n.Map.Keyboard=n.Handler.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61],zoomOut:[189,109,173]},initialize:function(t){this._map=t,this._setPanOffset(t.options.keyboardPanOffset),this._setZoomOffset(t.options.keyboardZoomOffset)},addHooks:function(){var t=this._map._container;-1===t.tabIndex&&(t.tabIndex="0"),n.DomEvent.on(t,"focus",this._onFocus,this).on(t,"blur",this._onBlur,this).on(t,"mousedown",this._onMouseDown,this),this._map.on("focus",this._addHooks,this).on("blur",this._removeHooks,this)},removeHooks:function(){this._removeHooks();var t=this._map._container;n.DomEvent.off(t,"focus",this._onFocus,this).off(t,"blur",this._onBlur,this).off(t,"mousedown",this._onMouseDown,this),this._map.off("focus",this._addHooks,this).off("blur",this._removeHooks,this)},_onMouseDown:function(){this._focused||this._map._container.focus()},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanOffset:function(t){var e,i,n=this._panKeys={},o=this.keyCodes;for(e=0,i=o.left.length;i>e;e++)n[o.left[e]]=[-1*t,0];for(e=0,i=o.right.length;i>e;e++)n[o.right[e]]=[t,0];for(e=0,i=o.down.length;i>e;e++)n[o.down[e]]=[0,t];for(e=0,i=o.up.length;i>e;e++)n[o.up[e]]=[0,-1*t]},_setZoomOffset:function(t){var e,i,n=this._zoomKeys={},o=this.keyCodes;for(e=0,i=o.zoomIn.length;i>e;e++)n[o.zoomIn[e]]=t;for(e=0,i=o.zoomOut.length;i>e;e++)n[o.zoomOut[e]]=-t},_addHooks:function(){n.DomEvent.on(e,"keydown",this._onKeyDown,this)},_removeHooks:function(){n.DomEvent.off(e,"keydown",this._onKeyDown,this)},_onKeyDown:function(t){var e=t.keyCode,i=this._map;if(this._panKeys.hasOwnProperty(e))i.panBy(this._panKeys[e]),i.options.maxBounds&&i.panInsideBounds(i.options.maxBounds);else{if(!this._zoomKeys.hasOwnProperty(e))return;i.setZoom(i.getZoom()+this._zoomKeys[e])}n.DomEvent.stop(t)}}),n.Map.addInitHook("addHandler","keyboard",n.Map.Keyboard),n.Handler.MarkerDrag=n.Handler.extend({initialize:function(t){this._marker=t},addHooks:function(){var t=this._marker._icon;this._draggable||(this._draggable=new n.Draggable(t,t).on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this)),this._draggable.enable()},removeHooks:function(){this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){this._marker.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(){var t=this._marker,e=t._shadow,i=n.DomUtil.getPosition(t._icon),o=t._map.layerPointToLatLng(i);e&&n.DomUtil.setPosition(e,i),t._latlng=o,t.fire("move",{latlng:o}).fire("drag")},_onDragEnd:function(){this._marker.fire("moveend").fire("dragend")}}),n.Handler.PolyEdit=n.Handler.extend({options:{icon:new n.DivIcon({iconSize:new n.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"})},initialize:function(t,e){this._poly=t,n.setOptions(this,e)},addHooks:function(){this._poly._map&&(this._markerGroup||this._initMarkers(),this._poly._map.addLayer(this._markerGroup))},removeHooks:function(){this._poly._map&&(this._poly._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers)},updateMarkers:function(){this._markerGroup.clearLayers(),this._initMarkers()},_initMarkers:function(){this._markerGroup||(this._markerGroup=new n.LayerGroup),this._markers=[];var t,e,i,o,s=this._poly._latlngs;for(t=0,i=s.length;i>t;t++)o=this._createMarker(s[t],t),o.on("click",this._onMarkerClick,this),this._markers.push(o);var a,r;for(t=0,e=i-1;i>t;e=t++)(0!==t||n.Polygon&&this._poly instanceof n.Polygon)&&(a=this._markers[e],r=this._markers[t],this._createMiddleMarker(a,r),this._updatePrevNext(a,r))},_createMarker:function(t,e){var i=new n.Marker(t,{draggable:!0,icon:this.options.icon});return i._origLatLng=t,i._index=e,i.on("drag",this._onMarkerDrag,this),i.on("dragend",this._fireEdit,this),this._markerGroup.addLayer(i),i},_fireEdit:function(){this._poly.fire("edit")},_onMarkerDrag:function(t){var e=t.target;n.extend(e._origLatLng,e._latlng),e._middleLeft&&e._middleLeft.setLatLng(this._getMiddleLatLng(e._prev,e)),e._middleRight&&e._middleRight.setLatLng(this._getMiddleLatLng(e,e._next)),this._poly.redraw()},_onMarkerClick:function(t){if(!(3>this._poly._latlngs.length)){var e=t.target,i=e._index;this._markerGroup.removeLayer(e),this._markers.splice(i,1),this._poly.spliceLatLngs(i,1),this._updateIndexes(i,-1),this._updatePrevNext(e._prev,e._next),e._middleLeft&&this._markerGroup.removeLayer(e._middleLeft),e._middleRight&&this._markerGroup.removeLayer(e._middleRight),e._prev&&e._next?this._createMiddleMarker(e._prev,e._next):e._prev?e._next||(e._prev._middleRight=null):e._next._middleLeft=null,this._poly.fire("edit")}},_updateIndexes:function(t,e){this._markerGroup.eachLayer(function(i){i._index>t&&(i._index+=e)})},_createMiddleMarker:function(t,e){var i,n,o,s=this._getMiddleLatLng(t,e),a=this._createMarker(s);a.setOpacity(.6),t._middleRight=e._middleLeft=a,n=function(){var n=e._index;a._index=n,a.off("click",i).on("click",this._onMarkerClick,this),s.lat=a.getLatLng().lat,s.lng=a.getLatLng().lng,this._poly.spliceLatLngs(n,0,s),this._markers.splice(n,0,a),a.setOpacity(1),this._updateIndexes(n,1),e._index++,this._updatePrevNext(t,a),this._updatePrevNext(a,e)},o=function(){a.off("dragstart",n,this),a.off("dragend",o,this),this._createMiddleMarker(t,a),this._createMiddleMarker(a,e)},i=function(){n.call(this),o.call(this),this._poly.fire("edit")},a.on("click",i,this).on("dragstart",n,this).on("dragend",o,this),this._markerGroup.addLayer(a)},_updatePrevNext:function(t,e){t&&(t._next=e),e&&(e._prev=t)},_getMiddleLatLng:function(t,e){var i=this._poly._map,n=i.latLngToLayerPoint(t.getLatLng()),o=i.latLngToLayerPoint(e.getLatLng());return i.layerPointToLatLng(n._add(o)._divideBy(2))}}),n.Polyline.addInitHook(function(){n.Handler.PolyEdit&&(this.editing=new n.Handler.PolyEdit(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()})}),n.Control=n.Class.extend({options:{position:"topright"},initialize:function(t){n.setOptions(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this +},addTo:function(t){this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),o=t._controlCorners[i];return n.DomUtil.addClass(e,"leaflet-control"),-1!==i.indexOf("bottom")?o.insertBefore(e,o.firstChild):o.appendChild(e),this},removeFrom:function(t){var e=this.getPosition(),i=t._controlCorners[e];return i.removeChild(this._container),this._map=null,this.onRemove&&this.onRemove(t),this}}),n.control=function(t){return new n.Control(t)},n.Map.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.removeFrom(this),this},_initControlPos:function(){function t(t,s){var a=i+t+" "+i+s;e[t+s]=n.DomUtil.create("div",a,o)}var e=this._controlCorners={},i="leaflet-",o=this._controlContainer=n.DomUtil.create("div",i+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")}}),n.Control.Zoom=n.Control.extend({options:{position:"topleft"},onAdd:function(t){var e="leaflet-control-zoom",i="leaflet-bar",o=i+"-part",s=n.DomUtil.create("div",e+" "+i);return this._map=t,this._zoomInButton=this._createButton("+","Zoom in",e+"-in "+o+" "+o+"-top",s,this._zoomIn,this),this._zoomOutButton=this._createButton("-","Zoom out",e+"-out "+o+" "+o+"-bottom",s,this._zoomOut,this),t.on("zoomend",this._updateDisabled,this),s},onRemove:function(t){t.off("zoomend",this._updateDisabled,this)},_zoomIn:function(t){this._map.zoomIn(t.shiftKey?3:1)},_zoomOut:function(t){this._map.zoomOut(t.shiftKey?3:1)},_createButton:function(t,e,i,o,s,a){var r=n.DomUtil.create("a",i,o);r.innerHTML=t,r.href="#",r.title=e;var h=n.DomEvent.stopPropagation;return n.DomEvent.on(r,"click",h).on(r,"mousedown",h).on(r,"dblclick",h).on(r,"click",n.DomEvent.preventDefault).on(r,"click",s,a),r},_updateDisabled:function(){var t=this._map,e="leaflet-control-zoom-disabled";n.DomUtil.removeClass(this._zoomInButton,e),n.DomUtil.removeClass(this._zoomOutButton,e),t._zoom===t.getMinZoom()&&n.DomUtil.addClass(this._zoomOutButton,e),t._zoom===t.getMaxZoom()&&n.DomUtil.addClass(this._zoomInButton,e)}}),n.Map.mergeOptions({zoomControl:!0}),n.Map.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new n.Control.Zoom,this.addControl(this.zoomControl))}),n.control.zoom=function(t){return new n.Control.Zoom(t)},n.Control.Attribution=n.Control.extend({options:{position:"bottomright",prefix:'Powered by Leaflet'},initialize:function(t){n.setOptions(this,t),this._attributions={}},onAdd:function(t){return this._container=n.DomUtil.create("div","leaflet-control-attribution"),n.DomEvent.disableClickPropagation(this._container),t.on("layeradd",this._onLayerAdd,this).on("layerremove",this._onLayerRemove,this),this._update(),this._container},onRemove:function(t){t.off("layeradd",this._onLayerAdd).off("layerremove",this._onLayerRemove)},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):i},removeAttribution:function(t){return t?(this._attributions[t]--,this._update(),this):i},_update:function(){if(this._map){var t=[];for(var e in this._attributions)this._attributions.hasOwnProperty(e)&&this._attributions[e]&&t.push(e);var i=[];this.options.prefix&&i.push(this.options.prefix),t.length&&i.push(t.join(", ")),this._container.innerHTML=i.join(" — ")}},_onLayerAdd:function(t){t.layer.getAttribution&&this.addAttribution(t.layer.getAttribution())},_onLayerRemove:function(t){t.layer.getAttribution&&this.removeAttribution(t.layer.getAttribution())}}),n.Map.mergeOptions({attributionControl:!0}),n.Map.addInitHook(function(){this.options.attributionControl&&(this.attributionControl=(new n.Control.Attribution).addTo(this))}),n.control.attribution=function(t){return new n.Control.Attribution(t)},n.Control.Scale=n.Control.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0,updateWhenIdle:!1},onAdd:function(t){this._map=t;var e="leaflet-control-scale",i=n.DomUtil.create("div",e),o=this.options;return this._addScales(o,e,i),t.on(o.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=n.DomUtil.create("div",e+"-line",i)),t.imperial&&(this._iScale=n.DomUtil.create("div",e+"-line",i))},_update:function(){var t=this._map.getBounds(),e=t.getCenter().lat,i=6378137*Math.PI*Math.cos(e*Math.PI/180),n=i*(t.getNorthEast().lng-t.getSouthWest().lng)/180,o=this._map.getSize(),s=this.options,a=0;o.x>0&&(a=n*(s.maxWidth/o.x)),this._updateScales(s,a)},_updateScales:function(t,e){t.metric&&e&&this._updateMetric(e),t.imperial&&e&&this._updateImperial(e)},_updateMetric:function(t){var e=this._getRoundNum(t);this._mScale.style.width=this._getScaleWidth(e/t)+"px",this._mScale.innerHTML=1e3>e?e+" m":e/1e3+" km"},_updateImperial:function(t){var e,i,n,o=3.2808399*t,s=this._iScale;o>5280?(e=o/5280,i=this._getRoundNum(e),s.style.width=this._getScaleWidth(i/e)+"px",s.innerHTML=i+" mi"):(n=this._getRoundNum(o),s.style.width=this._getScaleWidth(n/o)+"px",s.innerHTML=n+" ft")},_getScaleWidth:function(t){return Math.round(this.options.maxWidth*t)-10},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:1,e*i}}),n.control.scale=function(t){return new n.Control.Scale(t)},n.Control.Layers=n.Control.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0},initialize:function(t,e,i){n.setOptions(this,i),this._layers={},this._lastZIndex=0,this._handlingClick=!1;for(var o in t)t.hasOwnProperty(o)&&this._addLayer(t[o],o);for(o in e)e.hasOwnProperty(o)&&this._addLayer(e[o],o,!0)},onAdd:function(t){return this._initLayout(),this._update(),t.on("layeradd",this._onLayerChange,this).on("layerremove",this._onLayerChange,this),this._container},onRemove:function(t){t.off("layeradd",this._onLayerChange).off("layerremove",this._onLayerChange)},addBaseLayer:function(t,e){return this._addLayer(t,e),this._update(),this},addOverlay:function(t,e){return this._addLayer(t,e,!0),this._update(),this},removeLayer:function(t){var e=n.stamp(t);return delete this._layers[e],this._update(),this},_initLayout:function(){var t="leaflet-control-layers",e=this._container=n.DomUtil.create("div",t);n.Browser.touch?n.DomEvent.on(e,"click",n.DomEvent.stopPropagation):(n.DomEvent.disableClickPropagation(e),n.DomEvent.on(e,"mousewheel",n.DomEvent.stopPropagation));var i=this._form=n.DomUtil.create("form",t+"-list");if(this.options.collapsed){n.DomEvent.on(e,"mouseover",this._expand,this).on(e,"mouseout",this._collapse,this);var o=this._layersLink=n.DomUtil.create("a",t+"-toggle",e);o.href="#",o.title="Layers",n.Browser.touch?n.DomEvent.on(o,"click",n.DomEvent.stopPropagation).on(o,"click",n.DomEvent.preventDefault).on(o,"click",this._expand,this):n.DomEvent.on(o,"focus",this._expand,this),this._map.on("movestart",this._collapse,this)}else this._expand();this._baseLayersList=n.DomUtil.create("div",t+"-base",i),this._separator=n.DomUtil.create("div",t+"-separator",i),this._overlaysList=n.DomUtil.create("div",t+"-overlays",i),e.appendChild(i)},_addLayer:function(t,e,i){var o=n.stamp(t);this._layers[o]={layer:t,name:e,overlay:i},this.options.autoZIndex&&t.setZIndex&&(this._lastZIndex++,t.setZIndex(this._lastZIndex))},_update:function(){if(this._container){this._baseLayersList.innerHTML="",this._overlaysList.innerHTML="";var t=!1,e=!1;for(var i in this._layers)if(this._layers.hasOwnProperty(i)){var n=this._layers[i];this._addItem(n),e=e||n.overlay,t=t||!n.overlay}this._separator.style.display=e&&t?"":"none"}},_onLayerChange:function(t){var e=n.stamp(t.layer);this._layers[e]&&!this._handlingClick&&this._update()},_createRadioElement:function(t,i){var n='t;t++)e=o[t],i=this._layers[e.layerId],e.checked&&!this._map.hasLayer(i.layer)?(this._map.addLayer(i.layer),i.overlay||(n=i.layer)):!e.checked&&this._map.hasLayer(i.layer)&&this._map.removeLayer(i.layer);n&&(this._map.setZoom(this._map.getZoom()),this._map.fire("baselayerchange",{layer:n})),this._handlingClick=!1},_expand:function(){n.DomUtil.addClass(this._container,"leaflet-control-layers-expanded")},_collapse:function(){this._container.className=this._container.className.replace(" leaflet-control-layers-expanded","")}}),n.control.layers=function(t,e,i){return new n.Control.Layers(t,e,i)},n.PosAnimation=n.Class.extend({includes:n.Mixin.Events,run:function(t,e,i,o){this.stop(),this._el=t,this._inProgress=!0,this.fire("start"),t.style[n.DomUtil.TRANSITION]="all "+(i||.25)+"s cubic-bezier(0,0,"+(o||.5)+",1)",n.DomEvent.on(t,n.DomUtil.TRANSITION_END,this._onTransitionEnd,this),n.DomUtil.setPosition(t,e),n.Util.falseFn(t.offsetWidth),this._stepTimer=setInterval(n.bind(this.fire,this,"step"),50)},stop:function(){this._inProgress&&(n.DomUtil.setPosition(this._el,this._getPos()),this._onTransitionEnd(),n.Util.falseFn(this._el.offsetWidth))},_transformRe:/(-?[\d\.]+), (-?[\d\.]+)\)/,_getPos:function(){var e,i,o,s=this._el,a=t.getComputedStyle(s);return n.Browser.any3d?(o=a[n.DomUtil.TRANSFORM].match(this._transformRe),e=parseFloat(o[1]),i=parseFloat(o[2])):(e=parseFloat(a.left),i=parseFloat(a.top)),new n.Point(e,i,!0)},_onTransitionEnd:function(){n.DomEvent.off(this._el,n.DomUtil.TRANSITION_END,this._onTransitionEnd,this),this._inProgress&&(this._inProgress=!1,this._el.style[n.DomUtil.TRANSITION]="",clearInterval(this._stepTimer),this.fire("step").fire("end"))}}),n.Map.include({setView:function(t,e,i){e=this._limitZoom(e);var n=this._zoom!==e;if(this._loaded&&!i&&this._layers){this._panAnim&&this._panAnim.stop();var o=n?this._zoomToIfClose&&this._zoomToIfClose(t,e):this._panByIfClose(t);if(o)return clearTimeout(this._sizeTimer),this}return this._resetView(t,e),this},panBy:function(t,e,i){if(t=n.point(t),!t.x&&!t.y)return this;this._panAnim||(this._panAnim=new n.PosAnimation,this._panAnim.on({step:this._onPanTransitionStep,end:this._onPanTransitionEnd},this)),this.fire("movestart"),n.DomUtil.addClass(this._mapPane,"leaflet-pan-anim");var o=n.DomUtil.getPosition(this._mapPane).subtract(t)._round();return this._panAnim.run(this._mapPane,o,e||.25,i),this},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){n.DomUtil.removeClass(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_panByIfClose:function(t){var e=this._getCenterOffset(t)._floor();return this._offsetIsWithinView(e)?(this.panBy(e),!0):!1},_offsetIsWithinView:function(t,e){var i=e||1,n=this.getSize();return Math.abs(t.x)<=n.x*i&&Math.abs(t.y)<=n.y*i}}),n.PosAnimation=n.DomUtil.TRANSITION?n.PosAnimation:n.PosAnimation.extend({run:function(t,e,i,o){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(o||.5,.2),this._startPos=n.DomUtil.getPosition(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(),this._complete())},_animate:function(){this._animId=n.Util.requestAnimFrame(this._animate,this),this._step()},_step:function(){var t=+new Date-this._startTime,e=1e3*this._duration;e>t?this._runFrame(this._easeOut(t/e)):(this._runFrame(1),this._complete())},_runFrame:function(t){var e=this._startPos.add(this._offset.multiplyBy(t));n.DomUtil.setPosition(this._el,e),this.fire("step")},_complete:function(){n.Util.cancelAnimFrame(this._animId),this._inProgress=!1,this.fire("end")},_easeOut:function(t){return 1-Math.pow(1-t,this._easeOutPower)}}),n.Map.mergeOptions({zoomAnimation:n.DomUtil.TRANSITION&&!n.Browser.android23&&!n.Browser.mobileOpera}),n.DomUtil.TRANSITION&&n.Map.addInitHook(function(){n.DomEvent.on(this._mapPane,n.DomUtil.TRANSITION_END,this._catchTransitionEnd,this)}),n.Map.include(n.DomUtil.TRANSITION?{_zoomToIfClose:function(t,e){if(this._animatingZoom)return!0;if(!this.options.zoomAnimation)return!1;var i=this.getZoomScale(e),o=this._getCenterOffset(t)._divideBy(1-1/i);if(!this._offsetIsWithinView(o,1))return!1;n.DomUtil.addClass(this._mapPane,"leaflet-zoom-anim"),this.fire("movestart").fire("zoomstart"),this.fire("zoomanim",{center:t,zoom:e});var s=this._getCenterLayerPoint().add(o);return this._prepareTileBg(),this._runAnimation(t,e,i,s),!0},_catchTransitionEnd:function(){this._animatingZoom&&this._onZoomTransitionEnd()},_runAnimation:function(t,e,i,o,s){this._animateToCenter=t,this._animateToZoom=e,this._animatingZoom=!0,n.Draggable&&(n.Draggable._disabled=!0);var a=n.DomUtil.TRANSFORM,r=this._tileBg;clearTimeout(this._clearTileBgTimer),n.Util.falseFn(r.offsetWidth);var h=n.DomUtil.getScaleString(i,o),l=r.style[a];r.style[a]=s?l+" "+h:h+" "+l},_prepareTileBg:function(){var t=this._tilePane,e=this._tileBg;if(e&&this._getLoadedTilesPercentage(e)>.5&&.5>this._getLoadedTilesPercentage(t))return t.style.visibility="hidden",t.empty=!0,this._stopLoadingImages(t),i;e||(e=this._tileBg=this._createPane("leaflet-tile-pane",this._mapPane),e.style.zIndex=1),e.style[n.DomUtil.TRANSFORM]="",e.style.visibility="hidden",e.empty=!0,t.empty=!1,this._tilePane=this._panes.tilePane=e;var o=this._tileBg=t;n.DomUtil.addClass(o,"leaflet-zoom-animated"),this._stopLoadingImages(o)},_getLoadedTilesPercentage:function(t){var e,i,n=t.getElementsByTagName("img"),o=0;for(e=0,i=n.length;i>e;e++)n[e].complete&&o++;return o/i},_stopLoadingImages:function(t){var e,i,o,s=Array.prototype.slice.call(t.getElementsByTagName("img"));for(e=0,i=s.length;i>e;e++)o=s[e],o.complete||(o.onload=n.Util.falseFn,o.onerror=n.Util.falseFn,o.src=n.Util.emptyImageUrl,o.parentNode.removeChild(o))},_onZoomTransitionEnd:function(){this._restoreTileFront(),n.DomUtil.removeClass(this._mapPane,"leaflet-zoom-anim"),n.Util.falseFn(this._tileBg.offsetWidth),this._animatingZoom=!1,this._resetView(this._animateToCenter,this._animateToZoom,!0,!0),n.Draggable&&(n.Draggable._disabled=!1)},_restoreTileFront:function(){this._tilePane.innerHTML="",this._tilePane.style.visibility="",this._tilePane.style.zIndex=2,this._tileBg.style.zIndex=1},_clearTileBg:function(){this._animatingZoom||this.touchZoom._zooming||(this._tileBg.innerHTML="")}}:{}),n.Map.include({_defaultLocateOptions:{watch:!1,setView:!1,maxZoom:1/0,timeout:1e4,maximumAge:0,enableHighAccuracy:!1},locate:function(t){if(t=this._locationOptions=n.extend(this._defaultLocateOptions,t),!navigator.geolocation)return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var e=n.bind(this._handleGeolocationResponse,this),i=n.bind(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch(this._locationWatchId),this},_handleGeolocationError:function(t){var e=t.code,i=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout");this._locationOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+i+"."})},_handleGeolocationResponse:function(t){var e=180*t.coords.accuracy/4e7,i=2*e,o=t.coords.latitude,s=t.coords.longitude,a=new n.LatLng(o,s),r=new n.LatLng(o-e,s-i),h=new n.LatLng(o+e,s+i),l=new n.LatLngBounds(r,h),u=this._locationOptions;if(u.setView){var c=Math.min(this.getBoundsZoom(l),u.maxZoom);this.setView(a,c)}this.fire("locationfound",{latlng:a,bounds:l,accuracy:t.coords.accuracy})}})})(this,document); \ No newline at end of file -- cgit v1.2.3 From 273c6c037a87c1ecff8bc0630b0254b74da1ac01 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 1 Apr 2013 14:49:49 +0200 Subject: Change settings to allow different JS map renderer --- chimere/settings.sample.py | 278 +++++++++++++++++++++ chimere/templates/chimere/blocks/head_chimere.html | 4 +- chimere/templatetags/chimere_tags.py | 8 +- chimere/widgets.py | 25 +- 4 files changed, 299 insertions(+), 16 deletions(-) create mode 100644 chimere/settings.sample.py diff --git a/chimere/settings.sample.py b/chimere/settings.sample.py new file mode 100644 index 0000000..6db1464 --- /dev/null +++ b/chimere/settings.sample.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Don't edit this file: +# overload all theses settings in your local_settings.py file + +import os + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +# Django settings for chimere project. +PROJECT_NAME = 'Chimere' +ROOT_PATH = os.path.realpath(os.path.dirname(__file__)) + "/" + +EMAIL_HOST = 'localhost' +STATIC_URL = '/static/' +STATIC_ROOT = ROOT_PATH + 'static/' + +TINYMCE_URL = '/tinymce/' +JQUERY_JS_URLS = ('/javascript/jquery/jquery.js', + '/javascript/jquery-ui/jquery-ui.js',) +JQUERY_CSS_URLS = ('/javascript/jquery-ui/css/smoothness/jquery-ui.css', + '/javascript/jquery-ui-themes/base/jquery.ui.all.css') + +GPSBABEL = '/usr/bin/gpsbabel' +GPSBABEL_OPTIONS = 'simplify,crosstrack,error=0.005k' # simplify with an error of 5 meters +#GPSBABEL_OPTIONS = 'simplify,count=100' + +## chimere specific ## +CHIMERE_DEFAULT_ZOOM = 10 +# center of the map +CHIMERE_DEFAULT_CENTER = (-1.679444, 48.114722) +# projection used by the main map +# most public map providers use spherical mercator : 900913 +CHIMERE_EPSG_PROJECTION = 900913 +# projection displayed to the end user by openlayers +# chimere use the same projection to save its data in the database +CHIMERE_EPSG_DISPLAY_PROJECTION = 4326 +# display of shortcuts for areas +CHIMERE_DISPLAY_AREAS = True +# number of day before an event to display +# if equal to 0: disable event management +# if you change this value from 0 to a value in a production environnement +# don't forget to run the upgrade.py script to create appropriate fields in +# the database +CHIMERE_DAYS_BEFORE_EVENT = 30 +# allow feeds +CHIMERE_FEEDS = True + +CHIMERE_ICON_WIDTH = 21 +CHIMERE_ICON_HEIGHT = 25 +CHIMERE_ICON_OFFSET_X = -10 +CHIMERE_ICON_OFFSET_Y = -25 + +# display picture inside the description by default or inside a galery? +CHIMERE_MINIATURE_BY_DEFAULT = False + +# JS definition of the default map (for admin and when no map are defined in +# the application) +# cf. OpenLayers documentation for more details +CHIMERE_DEFAULT_MAP_LAYER = "new OpenLayers.Layer.OSM.Mapnik('Mapnik')" # OSM mapnik map + +CHIMERE_XAPI_URL = 'http://open.mapquestapi.com/xapi/api/0.6/' +CHIMERE_OSM_API_URL = 'api06.dev.openstreetmap.org' # test URL +CHIMERE_OSM_USER = 'test' +CHIMERE_OSM_PASSWORD = 'test' + +# as the web server need to be reloaded when property models are changed +# it could be a good idea to hide it to an admin who could'nt do that +CHIMERE_HIDE_PROPERTYMODEL = False + +# encoding for shapefile import +CHIMERE_SHAPEFILE_ENCODING = 'ISO-8859-1' + +# thumbnail +CHIMERE_THUMBS_SCALE_HEIGHT=250 +CHIMERE_THUMBS_SCALE_WIDTH=None + +CHIMERE_CSV_ENCODING = 'ISO-8859-1' + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'NAME': 'ratatouille', + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'HOST': 'localhost', + 'PORT': '5432', + 'USER': 'ratatouille', + 'PASSWORD': 'wiki', + }, +} + +# Local time zone for this installation. Choices can be found here: +# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +# although not all variations may be possible on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/Paris' + +# Language code for this installation. All choices can be found here: +# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes +# http://blogs.law.harvard.edu/tech/stories/storyReader$15 +LANGUAGE_CODE = 'fr-fr' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True +USE_L10N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = ROOT_PATH + 'media/' + +# URL that handles the media served from MEDIA_ROOT. +# Example: "http://media.lawrence.com" +MEDIA_URL = '/media/' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.doc.XViewMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware' +) + +ROOT_URLCONF = 'chimere_example_project.urls' + +TEMPLATE_DIRS = [ + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + ROOT_PATH + 'templates', + ROOT_PATH + '../chimere/templates', +] + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.request", +) + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.admin', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.gis', + 'django.contrib.staticfiles', +] + +# celery +try: + import djcelery + import kombu + djcelery.setup_loader() + BROKER_URL = 'django://' + INSTALLED_APPS += ['kombu.transport.django', + 'djcelery'] +except ImportError: + # some import and export will not be available + pass + +INSTALLED_APPS += [ + 'south', + 'chimere', +# 'chimere.scripts', # activate it if you want to use old migration scripts +] + +LOG_PATH = '/var/log/django/' + +CHIMERE_VIEW_RENDERER = 'openlayers' # 'openlayers' or 'leaflet' +CHIMERE_EDIT_RENDERER = 'openlayers' # 'openlayers' + +try: + from local_settings import * +except ImportError, e: + print 'Unable to load local_settings.py:', e + +if 'LOGGING' not in globals(): + global LOGGING + LOGGING = {'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + # Include the default Django email handler for errors + # This is what you'd get without configuring logging at all. + 'mail_admins': { + 'class': 'django.utils.log.AdminEmailHandler', + 'level': 'ERROR', + # But the emails are plain text by default - HTML is nicer + 'include_html': True, + }, + # Log to a text file that can be rotated by logrotate + 'logfile': { + 'class': 'logging.handlers.WatchedFileHandler', + 'filename': LOG_PATH + 'chimere.log' + }, + }, + 'loggers': { + # Again, default Django configuration to email unhandled exceptions + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + # Might as well log any errors anywhere else in Django + 'django': { + 'handlers': ['logfile'], + 'level': 'ERROR', + 'propagate': False, + }, + # Your own app - this assumes all your logger names start with "myapp." + 'chimere': { + 'handlers': ['logfile'], + 'level': 'WARNING', # Or maybe INFO or DEBUG + 'propogate': False + }, + }, + } + + +if 'CHIMERE_SHARE_NETWORKS' not in globals(): + # after the locals to get the right STATIC_URL + + # share with + global CHIMERE_SHARE_NETWORKS + CHIMERE_SHARE_NETWORKS = ( + ("Email", 'mailto:?subject=%(text)s&body=%(url)s', + STATIC_URL + 'chimere/img/email.png'), + ("Facebook", 'http://www.facebook.com/sharer.php?t=%(text)s&u=%(url)s', + STATIC_URL + 'chimere/img/facebook.png'), + ("Twitter", 'http://twitter.com/home?status=%(text)s %(url)s', + STATIC_URL + 'chimere/img/twitter.png'), + ("Identi.ca", 'http://identi.ca/index.php?action=newnotice&status_textarea=%(text)s %(url)s', + STATIC_URL + 'chimere/img/identica.png'), + ) + +if 'MAP_JS_URLS' not in globals(): + global MAP_JS_URLS + MAP_JS_URLS = { + 'openlayers':[ + STATIC_URL + "openlayers/OpenLayers.js", + STATIC_URL + "openlayers/SimplePanZoom.js", + "http://www.openstreetmap.org/openlayers/OpenStreetMap.js"], + 'leaflet':[ + STATIC_URL + "leaflet/leaflet.js" + ] + } + +if 'MAP_CSS_URLS' not in globals(): + global MAP_CSS_URLS + MAP_CSS_URLS = { + 'openlayers':["http://www.openlayers.org/api/theme/default/style.css"], + 'leaflet':[ + STATIC_URL + "leaflet/leaflet.css", + STATIC_URL + "leaflet/leaflet.ie.css" + ] + } + diff --git a/chimere/templates/chimere/blocks/head_chimere.html b/chimere/templates/chimere/blocks/head_chimere.html index 147d05b..8863f10 100644 --- a/chimere/templates/chimere/blocks/head_chimere.html +++ b/chimere/templates/chimere/blocks/head_chimere.html @@ -1,6 +1,6 @@ -{% for css_url in OSM_CSS_URLS %} +{% for css_url in MAP_CSS_URLS %} {% endfor %} -{% for js_url in OSM_JS_URLS %} +{% for js_url in MAP_JS_URLS %} {% endfor %} {% if routing %}{% endif %} diff --git a/chimere/templatetags/chimere_tags.py b/chimere/templatetags/chimere_tags.py index ed835a8..06e72ed 100644 --- a/chimere/templatetags/chimere_tags.py +++ b/chimere/templatetags/chimere_tags.py @@ -119,7 +119,7 @@ def head_jme(context): return context_data @register.inclusion_tag('chimere/blocks/head_chimere.html', takes_context=True) -def head_chimere(context): +def head_chimere(context, view=True): """ Create context and display head elements (js, css, etc.) for chimere. """ @@ -130,6 +130,8 @@ def head_chimere(context): area = Area.objects.get(urn=area_name) except ObjectDoesNotExist: pass + map_renderer = settings.CHIMERE_VIEW_RENDERER if view \ + else settings.CHIMERE_EDIT_RENDERER context_data = { "STATIC_URL": settings.STATIC_URL, "MEDIA_URL": settings.MEDIA_URL, @@ -141,8 +143,8 @@ def head_chimere(context): "DEFAULT_CENTER": settings.CHIMERE_DEFAULT_CENTER, "DEFAULT_ZOOM": settings.CHIMERE_DEFAULT_ZOOM, "MAP_LAYER": settings.CHIMERE_DEFAULT_MAP_LAYER, - "OSM_CSS_URLS": settings.OSM_CSS_URLS, - "OSM_JS_URLS": settings.OSM_JS_URLS, + "MAP_CSS_URLS": settings.MAP_CSS_URLS[map_renderer], + "MAP_JS_URLS": settings.MAP_JS_URLS[map_renderer], 'routing':settings.CHIMERE_ENABLE_ROUTING } return context_data diff --git a/chimere/widgets.py b/chimere/widgets.py index 45161e1..f39248c 100644 --- a/chimere/widgets.py +++ b/chimere/widgets.py @@ -248,10 +248,11 @@ class PointChooserWidget(forms.TextInput): """ class Media: css = { - "all": settings.OSM_CSS_URLS + \ + "all": settings.MAP_CSS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ ["%schimere/css/forms.css" % settings.STATIC_URL,] } - js = settings.OSM_JS_URLS + list(settings.JQUERY_JS_URLS) + \ + js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ + list(settings.JQUERY_JS_URLS) + \ ["%schimere/js/jquery.chimere.js" % settings.STATIC_URL] def render(self, name, value, attrs=None, area_name=''): @@ -340,10 +341,12 @@ class RouteChooserWidget(forms.TextInput): Manage the edition of route on a map """ class Media: - css = {"all": settings.OSM_CSS_URLS + \ - ["%schimere/css/forms.css" % settings.STATIC_URL,] + css = { + "all": settings.MAP_CSS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ + ["%schimere/css/forms.css" % settings.STATIC_URL,] } - js = settings.OSM_JS_URLS + list(settings.JQUERY_JS_URLS) + \ + js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ + list(settings.JQUERY_JS_URLS) + \ ["%schimere/js/jquery.chimere.js" % settings.STATIC_URL, "%schimere/js/edit_route_map.js" % settings.STATIC_URL, "%schimere/js/base.js" % settings.STATIC_URL,] @@ -477,11 +480,11 @@ class AreaWidget(forms.TextInput): """ class Media: css = { - "all": settings.OSM_CSS_URLS + \ + "all": settings.MAP_CSS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ ["%schimere/css/forms.css" % settings.STATIC_URL,] } - js = settings.OSM_JS_URLS + [ - "%schimere/js/edit_area.js" % settings.STATIC_URL, + js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ + ["%schimere/js/edit_area.js" % settings.STATIC_URL, "%schimere/js/base.js" % settings.STATIC_URL,] def get_bounding_box_from_value(self, value): @@ -576,11 +579,11 @@ class ImportFiltrWidget(AreaWidget): """ class Media: css = { - "all": settings.OSM_CSS_URLS + \ + "all": settings.MAP_CSS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ ["%schimere/css/forms.css" % settings.STATIC_URL,] } - js = settings.OSM_JS_URLS + [ - "%schimere/js/edit_area.js" % settings.STATIC_URL, + js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ + ["%schimere/js/edit_area.js" % settings.STATIC_URL, "%schimere/js/base.js" % settings.STATIC_URL,] def render(self, name, value, attrs=None): -- cgit v1.2.3 From d1a9401ff23b5abdb44b889b663711e9625eea27 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 1 Apr 2013 18:44:39 +0200 Subject: Better management of map JS files --- chimere/settings.sample.py | 16 +- chimere/static/chimere/js/jquery.chimere-ol.js | 1695 ++++++++++++++++++++ chimere/static/chimere/js/jquery.chimere.js | 1695 -------------------- chimere/templates/chimere/blocks/head_chimere.html | 2 - chimere/templates/chimere/main_map.html | 1 - chimere/widgets.py | 6 +- 6 files changed, 1707 insertions(+), 1708 deletions(-) create mode 100644 chimere/static/chimere/js/jquery.chimere-ol.js delete mode 100644 chimere/static/chimere/js/jquery.chimere.js diff --git a/chimere/settings.sample.py b/chimere/settings.sample.py index c8fdb91..2e5ad40 100644 --- a/chimere/settings.sample.py +++ b/chimere/settings.sample.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# This file is an example of settings.py for a Chimère project +# Don't edit this file: +# overload all theses settings in your local_settings.py file import os _ = lambda s: s @@ -68,13 +69,13 @@ CHIMERE_OSM_API_URL = 'api06.dev.openstreetmap.org' # test URL CHIMERE_OSM_USER = 'test' CHIMERE_OSM_PASSWORD = 'test' +# encoding for shapefile import +CHIMERE_SHAPEFILE_ENCODING = 'ISO-8859-1' + # as the web server need to be reloaded when property models are changed # it could be a good idea to hide it to an admin who could'nt do that CHIMERE_HIDE_PROPERTYMODEL = False -# encoding for shapefile import -CHIMERE_SHAPEFILE_ENCODING = 'ISO-8859-1' - # enable routing in Chimère CHIMERE_ENABLE_ROUTING = False @@ -288,9 +289,11 @@ if 'MAP_JS_URLS' not in globals(): 'openlayers':[ STATIC_URL + "openlayers/OpenLayers.js", STATIC_URL + "openlayers/SimplePanZoom.js", - "http://www.openstreetmap.org/openlayers/OpenStreetMap.js"], + "http://www.openstreetmap.org/openlayers/OpenStreetMap.js", + STATIC_URL + "chimere/js/jquery.chimere-ol.js"], 'leaflet':[ - STATIC_URL + "leaflet/leaflet.js" + STATIC_URL + "leaflet/leaflet.js", + STATIC_URL + "chimere/js/jquery.chimere-leaflet.js" ] } @@ -303,3 +306,4 @@ if 'MAP_CSS_URLS' not in globals(): STATIC_URL + "leaflet/leaflet.ie.css" ] } + diff --git a/chimere/static/chimere/js/jquery.chimere-ol.js b/chimere/static/chimere/js/jquery.chimere-ol.js new file mode 100644 index 0000000..2e2d238 --- /dev/null +++ b/chimere/static/chimere/js/jquery.chimere-ol.js @@ -0,0 +1,1695 @@ +/* Copyright (C) 2008-2012 Étienne Loks + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +See the file COPYING for details. +*/ + +/* Add OpenLayers MapQuest layer management */ +OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { + name: "MapQuestOSM", + sphericalMercator: true, + url: ' http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png', + clone: function(obj) { + if (obj == null) { + obj = new OpenLayers.Layer.OSM( + this.name, this.url, this.getOptions()); + } + obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); + return obj; + }, + CLASS_NAME: "OpenLayers.Layer.MapQuestOSM" +}); + +/* +* Little hasattr helper +*/ +(function ($) { + $.hasattr = function (key, arr) { + var v = arr[key]; + if (typeof v === "undefined") + return false; + else + return v; }; +})( jQuery ); + +(function ($) { + /* + * Chimere jQuery plugin + */ + /* + * Default settings + */ + var defaults = { + restricted_extent: false, + permalink_label: null, + permalink_div: null, + permalink: null, // OL Control, could be overrided + map_layers: null, + selected_map_layer: null, + dynamic_categories: false, + display_submited: false, + display_feature: null, + display_route: null, + area_id: null, + checked_categories: [], + zoom: null, + lat: null, + lon: null, + simple: false, + routing_start_lat: null, + routing_start_lon: null, + routing_end_lat: null, + routing_end_lon: null, + routing_steps_lonlat: null, + // Provide this function to make a custom click event on the marker + on_marker_click: null, + // Provide this function to override the feature detail display + display_feature_detail_fx: null, + // Provide this function for overriding the getSubcategories default + get_subcategories_fx: null, + hide_popup_fx: null, + // if leave to false every click on the map hide the pop-up + explicit_popup_hide: false, + controls:[new OpenLayers.Control.Navigation(), + new OpenLayers.Control.PanPanel(), + new OpenLayers.Control.ZoomPanel(), + new OpenLayers.Control.ScaleLine()], + popupClass: OpenLayers.Popup.FramedCloud, + popupContentFull: false, // if true the detail is inside the popup + category_accordion: true, // category opening behave like an accordion + maxResolution: 156543.0399, + units: 'm', + projection: new OpenLayers.Projection('EPSG:4326'), + theme: null, + enable_clustering: false, + routing: false, // enable routing management + routing_panel_open: function(){ + $('#chimere_itinerary_panel').dialog('open'); + }, + current_feature: null, // To store the active POI + current_control: null, // To store the current control + current_popup: null, // To store the current POI popup displayed + current_category: null, // To store the current category clicked in list + current_route_feature: null, // To store the current route find by routing + itinerary_step_number:0, // current step number + icon_offset: new OpenLayers.Pixel(0, 0), + edition: false, // edition mode + edition_type_is_route: false, // route or POI edition + default_icon: new OpenLayers.Icon( + 'http://www.openlayers.org/dev/img/marker-green.png', + new OpenLayers.Size(21, 25), + new OpenLayers.Pixel(-(21/2), -25)), + cluster_icon: null, + marker_hover_id:'marker_hover', + marker_hover_content_id:'marker_hover_content', + marker_hover_offset: null, + icon_start: null, + icon_step: null, + icon_end: null + }; + var settings = {}; + /* + * Publics methods + */ + var methods = { + /* + * Plugin init function + */ + init: function ( options ) { + /* Manage parameters */ + // not staticaly in default because of STATIC_URL init + if (defaults.cluster_icon == null && typeof STATIC_URL != 'undefined'){ + defaults.cluster_icon = new OpenLayers.Icon( + STATIC_URL+'chimere/img/marker-cluster.png', + new OpenLayers.Size(36, 39), + new OpenLayers.Pixel(-(36/2), -(39/2))); + } + if (defaults.icon_start == null && typeof STATIC_URL != 'undefined'){ + defaults.icon_start = new OpenLayers.Icon( + STATIC_URL + "chimere/img/flag-start.png", + new OpenLayers.Size(32, 32), + new OpenLayers.Pixel(0, -32)); + } + if (defaults.icon_step == null && typeof STATIC_URL != 'undefined'){ + defaults.icon_step = new OpenLayers.Icon( + STATIC_URL + "chimere/img/flag-step.png", + new OpenLayers.Size(32, 32), + new OpenLayers.Pixel(0, -32)); + } + if (defaults.icon_end == null && typeof STATIC_URL != 'undefined'){ + defaults.icon_end = new OpenLayers.Icon( + STATIC_URL + "chimere/img/flag-finish.png", + new OpenLayers.Size(32, 32), + new OpenLayers.Pixel(0, -32)); + } + settings = $.extend({}, defaults); + if ( options ) $.extend(settings, options); + var map_element = $(this).get(0); + var map_options = { + controls: settings.controls, + maxResolution: settings.maxResolution, + units: settings.units, + projection: settings.projection, + theme: settings.theme + }; + if (settings.restricted_extent){ + settings.restricted_extent.transform(EPSG_DISPLAY_PROJECTION, + EPSG_PROJECTION); + map_options['restrictedExtent'] = settings.restricted_extent; + } + + settings.current_position = null; + + /* Create map object */ + settings.map = map = new OpenLayers.Map(map_element, map_options); + + map.addControl(new OpenLayers.Control.Attribution()); + + /* Manage permalink */ + if (!settings.edition){ + if (settings.permalink == null && !settings.edition) { + var permalink_options = {}; + if (settings.permalink_element){ + // hard to understand from OL documentation... + permalink_options["div"] = settings.permalink_element; + } + settings.permalink = new OpenLayers.Control.Permalink( + permalink_options); + } + /* HACK new permalink createParams method + FIXME when facilities are given to personalize the permalink */ + var oldCreateParams = settings.permalink.createParams + var _createParams = function(center, zoom, layers) { + // Call normal method + var params = oldCreateParams(center, zoom, layers); + // Make specific params + params.checked_categories = settings.checked_categories; + params.display_submited = settings.display_submited; + if(settings.current_feature) + params.current_feature = settings.current_feature.pk; + if (settings.routing_start){ + lonlat = settings.routing_start.lonlat.clone().transform( + EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); + params.routing_start_lon = lonlat.lon; + params.routing_start_lat = lonlat.lat; + } + if (settings.routing_end){ + lonlat = settings.routing_end.lonlat.clone().transform( + EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); + params.routing_end_lon = lonlat.lon; + params.routing_end_lat = lonlat.lat; + } + if (settings.routing_steps){ + var steps = []; + for (var i = 0; i < settings.routing_steps.length; i++){ + lonlat = settings.routing_steps[i].lonlat.clone( + ).transform(EPSG_PROJECTION, + EPSG_DISPLAY_PROJECTION); + steps.push([lonlat.lon, lonlat.lat]); + } + params.routing_steps = steps; + } + return params; + } + // Force new createParams method + settings.permalink.createParams = _createParams; + settings.map.addControl(settings.permalink); + // update with the translated permalink label + if(settings.permalink_label && settings.permalink.div + && settings.permalink.div.childNodes.length > 0){ + settings.permalink.div.childNodes[0].textContent = settings.permalink_label; + } + } + /* Add Layers */ + settings.map.addLayers(settings.map_layers); + if (settings.map_layers.length > 1){ + settings.map.addControl(new OpenLayers.Control.LayerSwitcher( + {roundedCorner:false})); + } + /* select the default map layer */ + if (!settings.selected_map_layer){ + settings.selected_map_layer = 0; + } + settings.map.setBaseLayer( + settings.map_layers[settings.selected_map_layer]); + + /* manage the context menu */ + $('#map_menu_zoomin').bind("click", methods.zoomIn); + $('#map_menu_zoomout').bind("click", methods.zoomOut); + $('#map_menu_center').bind("click", methods.mapCenter); + /* manage the routing */ + if (settings.routing){ + settings.routing_start = null; + settings.routing_steps = new Array(); + settings.routing_end = null; + $('#map_menu_from').bind("click", methods.routingFrom); + $('#map_menu_step').bind("click", methods.routingAddStep); + $('#map_menu_to').bind("click", methods.routingTo); + $('#map_menu_clear').bind("click", methods.routingClear); + settings.layerRoute = new OpenLayers.Layer.Vector("Route Layer"); + settings.map.addLayer(settings.layerRoute); + settings.layerRouteMarker = new OpenLayers.Layer.Markers( + 'Route markers'); + settings.map.addLayer(settings.layerRouteMarker); + } + /* Vectors layer */ + settings.layerVectors = new OpenLayers.Layer.Vector("Vector Layer"); + settings.map.addLayer(settings.layerVectors); + settings.layerVectors.setOpacity(0.8); + if (settings.edition_type_is_route){ + settings.map.addControl(new OpenLayers.Control.DrawFeature( + settings.layerVectors, OpenLayers.Handler.Path)); + settings.map.addControl(new OpenLayers.Control.ModifyFeature( + settings.layerVectors, {clickout:false, toggle:false})); + } + + if (settings.enable_clustering){ + var style = new OpenLayers.Style({ + graphicTitle: "${name}", + externalGraphic: "${icon}", + graphicWidth: "${width}", + graphicHeight: "${height}", + graphicXOffset: "${offsetx}", + graphicYOffset: "${offsety}", + graphicOpacity: 1, + label: "${label}", + labelYOffset: "2", + fontSize:'1.3em' + }, { + context: { + name: function(feature) { + if(feature.cluster) { + feature.attributes.width = settings.cluster_icon.size.w; + feature.attributes.height = settings.cluster_icon.size.h; + feature.attributes.offsetx = settings.cluster_icon.offset.x; + feature.attributes.offsety = settings.cluster_icon.offset.y; + } else{ + var marker = feature.attributes.marker + feature.attributes.width = marker.icon.size.w; + feature.attributes.height = marker.icon.size.h; + feature.attributes.offsetx = settings.icon_offset.x; + feature.attributes.offsety = settings.icon_offset.y; + } + return feature.attributes.name; + }, + label: function(feature) { + // clustered features count or blank if feature is not a cluster + return feature.cluster ? feature.cluster.length : ""; + }, + icon: function(feature) { + if (feature.cluster){ + return settings.cluster_icon.url; + } else { + return STATIC_URL + 'chimere/img/empty.png'; + } + }, + width: function(feature) { return feature.attributes.width; }, + height: function(feature) { return feature.attributes.height; }, + offsetx: function(feature) { return feature.attributes.offsetx; }, + offsety: function(feature) { return feature.attributes.offsety; } + }}); + + + /* Cluster layer */ + settings.clustering = new OpenLayers.Strategy.Cluster({ + distance: 10, + threshold: 2}); + settings.layerCluster = new OpenLayers.Layer.Vector("Cluster layer", + {styleMap: new OpenLayers.StyleMap({'default': style}), + strategies: [settings.clustering]}); + settings.map.addLayer(settings.layerCluster); + + var highlightCtrl = new OpenLayers.Control.SelectFeature( + settings.layerCluster, { + hover: true, + highlightOnly: true, + eventListeners: { + featurehighlighted: function(e) { + if(e.feature.attributes.marker) + e.feature.attributes.marker.events.triggerEvent('mouseover'); + }, + featureunhighlighted: function(e) { + if(e.feature.attributes.marker) + e.feature.attributes.marker.events.triggerEvent('mouseout'); + } + } + }); + + var selectCtrl = new OpenLayers.Control.SelectFeature( + settings.layerCluster,{ + onSelect: methods.zoomOnCluster + }); + + settings.map.addControl(highlightCtrl); + settings.map.addControl(selectCtrl); + + highlightCtrl.activate(); + selectCtrl.activate(); + } + + /* Markers layer */ + settings.layerMarkers = new OpenLayers.Layer.Markers('POIs'); + settings.map.addLayer(settings.layerMarkers); + + if (settings.dynamic_categories){ + settings.map.events.register('moveend', settings.map, + methods.loadCategories); + } + /* TODO make a function */ + if (settings.display_submited) { + document.getElementById('display_submited_check').checked = true; + } + /* if we have some zoom and lon/lat from the init options */ + if (settings.zoom && settings.lon && settings.lat) { + var centerLonLat = new OpenLayers.LonLat(settings.lon, + settings.lat); + settings.map.setCenter(centerLonLat, settings.zoom); + } + /* if not zoom to the extent in cookies */ + else if (!methods.zoomToCurrentExtent(settings.map)){ + /* if no extent in cookies zoom to default */ + if(CENTER_LONLAT && DEFAULT_ZOOM){ + settings.map.setCenter(CENTER_LONLAT, DEFAULT_ZOOM); + } + } + + if (!settings.edition){ + if (settings.enable_clustering){ + settings.map.events.register('zoomend', null, + methods.cleanCluster); + } + methods.loadCategories(); + methods.loadGeoObjects(); + methods.activateContextMenu() + } else { + if (!settings.edition_type_is_route){ + methods.activateMarkerEdit(); + } else { + settings.layerVectors.events.register('featuremodified', + settings.layerVectors, helpers.updateRouteForm); + settings.layerVectors.events.register('featureadded', + settings.layerVectors, helpers.featureRouteCreated); + } + } + if (settings.routing_start_lon && settings.routing_start_lat){ + settings.current_position = new OpenLayers.LonLat( + settings.routing_start_lon, settings.routing_start_lat + ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); + methods.routingFrom(); + } + if (settings.routing_end_lon && settings.routing_end_lat){ + settings.current_position = new OpenLayers.LonLat( + settings.routing_end_lon, settings.routing_end_lat + ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); + methods.routingTo(); + } + if (settings.routing_steps_lonlat){ + for (var i = 0; i < settings.routing_steps_lonlat.length/2; i++) { + lon = settings.routing_steps_lonlat[i*2]; + lat = settings.routing_steps_lonlat[i*2+1]; + settings.current_position = new OpenLayers.LonLat(lon, lat + ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); + methods.routingAddStep(); + } + } + methods.preload_images(); + }, // end of init + /* Preload icons */ + preload_images: function(){ + if (typeof extra_url == 'undefined') return; + var uri = extra_url + "getAllCategories/"; + $.ajax({url: uri, + dataType: "json", + success: function (data) { + if (!data.categories){return} + for(var idx=0; idx span"); + if (parent.find('input[type=checkbox]:checked').length){ + parent_label.addClass('category-selected'); + } else { + parent_label.removeClass('category-selected'); + } + var master_check = parent.find("> input"); + if (parent.find('.subcategories input[type=checkbox]').length == + parent.find('.subcategories input[type=checkbox]:checked').length){ + master_check.attr('checked', 'checked'); + } else { + master_check.removeAttr('checked'); + } + + if($('#action-categories').length){ + if ($('#categories input[type=checkbox]:checked').length){ + $('#action-categories').addClass('category-selected'); + } else { + $('#action-categories').removeClass('category-selected'); + } + } + return master_check; + }; + var _init_categories = function () { + /* + * Add event listener in categories DOM elements + */ + $('#categories #ul_categories > li > input').bind("click", + function (e) { + methods.hidePopup(e); + _toggle_subcategories($(this)); + methods.loadGeoObjects(); + settings.permalink.updateLink(); + }); + $('.subcategories li input').bind("click", function (e) { + var c_name = $(this).attr('name'); + c_name = c_name.substr(c_name.lastIndexOf("_")+1); + if($(this).is(':checked')){ + methods.subcategory_detail(c_name); + } + var par = $(this).parent(); + if ($(this).attr('checked')){ + par.addClass('selected'); + } else { + par.removeClass('selected'); + } + methods.hidePopup(e); + methods.loadGeoObjects(); + _toggle_categories($(this)); + settings.permalink.updateLink(); + if ($('#layer_cat_'+c_name).length){ + $('#layer_cat_'+c_name).prop("checked", + this.checked); + } + }); + $('#display_submited_check').bind("click", function () { + methods.loadGeoObjects(); + settings.permalink.updateLink(); + }); + // Zoom to category + $(".zoom_to_category").bind("click", function (e) { + var id = this.id.substr(this.id.lastIndexOf("_")+1); + helpers.zoom_to_category(id); + }); + $(".zoom_to_subcategory").bind("click", function (e) { + var id = this.id.substr(this.id.lastIndexOf("_")+1); + helpers.zoom_to_subcategories([id]); + }); + $(".toggle_category").parent().bind("click", function (e) { + var item = $(this).children('.toggle_category'); + var id = item.attr('id').substr(item.attr('id').lastIndexOf("_")+1); + methods.toggle_category(id); + }); + } + var _reCheckCategories = function (){ + /* recheck categories on init or when a redraw occurs */ + if (!settings.checked_categories){ + return; + } + $('#frm_categories .subcategories input:checkbox').each(function(index){ + cat_id = $(this).attr('id').split('_').pop(); + if (settings.checked_categories.indexOf(parseInt(cat_id)) != -1) { + $(this).attr("checked", "checked"); + _toggle_categories($(this)); + methods.toggle_category(); + } else { + $(this).attr("checked", false); + } + }); + if (settings.display_submited == true){ + $('#display_submited_check').attr("checked", "checked"); + } + } + }, + /* + * Hide clusterized markers + */ + cleanCluster: function (){ + if (settings.map.getZoom() === 18) { + // Don't cluster at this level. No matter what. + settings.clustering.threshold = 1000; + } else { + settings.clustering.threshold = 2; + } + //settings.layerCluster.refresh({force:true}); + settings.clustering.recluster(); + var hidden_feature_idx = []; + if (settings.map.getZoom() != 18) { + for(var idx=0; idx -1){ + settings.layerMarkers.markers[j].display(false); + } else { + settings.layerMarkers.markers[j].display(true); + } + } + }, + /* + * Put a marker on the map + */ + addMarker: function (mark) { + /* + * Default Feature configuration + * This can be overrided in on_marker_click, using settings.current_feature + */ + var lat = mark.geometry.coordinates[1]; + var lon = mark.geometry.coordinates[0]; + var size = new OpenLayers.Size(mark.properties.icon_width, + mark.properties.icon_height); + var icon_url = MEDIA_URL + mark.properties.icon_path; + var icon_hover_url = ''; + if (mark.properties.icon_hover_path){ + var icon_hover_url = MEDIA_URL + mark.properties.icon_hover_path; + } + var iconclone = new OpenLayers.Icon(icon_url, size, + settings.icon_offset); + + var feature = new OpenLayers.Feature(settings.layerMarkers, + new OpenLayers.LonLat(lon, lat).transform( + EPSG_DISPLAY_PROJECTION, + EPSG_PROJECTION), + {icon:iconclone}); + feature.pk = mark.properties.pk; + feature.popupClass = settings.popupClass; + feature.data.popupContentHTML = "
"; + if (!settings.popupContentFull) { + feature.data.popupContentHTML += mark.properties.name; + } + feature.data.popupContentHTML += "
"; + feature.data.overflow = 'hidden'; + var marker = feature.createMarker(); + marker.pk = feature.pk; + marker.icon_url = icon_url; + marker.icon_hover_url = icon_hover_url; + marker.category_name = mark.properties.category_name; + /* manage markers events */ + var _popup = function() { + /* show the popup */ + if (settings.current_popup != null) { + settings.current_popup.hide(); + } + if (feature.popup == null) { + feature.popup = feature.createPopup(); + settings.map.addPopup(feature.popup); + } else { + feature.popup.toggle(); + } + settings.current_popup = feature.popup; + /* hide on click on the cloud */ + if (!settings.explicit_popup_hide){ + settings.current_popup.groupDiv.onclick = methods.hidePopup; + } + settings.permalink.updateLink(); + } + var _repan_popup = function(){ + /* re-pan manually */ + + // no clean way to detect if all the element are ready + // lack of better... + setTimeout( + function(){ + settings.current_popup.panIntoView(); + }, 1000); + } + + var markerClick = function (evt) { + settings.current_feature = feature; + methods.setCurrentPosition(feature.lonlat); + if ( settings.on_marker_click ) { + settings.on_marker_click(evt, mark, settings); + } + else + { + methods.center_on_feature(); + $('#chimere_map_menu').hide(); + // Default popup + if (feature.popup && feature.popup.visible()) { + if (settings.current_popup == feature.popup) { + feature.popup.hide(); + if (!settings.simple){ + $('#detail').hide(); + } + } else { + settings.current_popup.hide(); + _popup(); + methods.display_feature_detail(feature.pk); + _repan_popup(); + } + } else { + _popup(); + methods.display_feature_detail(feature.pk); + _repan_popup(); + } + } + OpenLayers.Event.stop(evt); + }; + var markerOver = function (evt) { + document.body.style.cursor='pointer'; + if (settings.current_feature && settings.current_feature.popup + && settings.current_feature.popup.visible()) return; + var marker = evt.object; + if (marker.icon_hover_url){ + marker.setUrl(marker.icon_hover_url); + } + px = settings.map.getPixelFromLonLat(marker.lonlat); + marker_hover = $('#'+settings.marker_hover_id); + marker_hover_content = $('#'+settings.marker_hover_content_id); + marker_hover_content.html(marker.category_name); + var map_position = $(settings.map.div).offset(); + + var width = marker_hover.width(); + width += parseInt(marker_hover.css("padding-left"), 10) + + parseInt(marker_hover.css("padding-right"), 10) + + parseInt(marker_hover.css("margin-left"), 10) + + parseInt(marker_hover.css("margin-right"), 10) + + parseInt(marker_hover.css("borderLeftWidth"), 10) + + parseInt(marker_hover.css("borderRightWidth"), 10); + var pos_x = px.x + map_position.left + - width/2 + 1; + if (settings.marker_hover_offset) + pos_x += settings.marker_hover_offset.x; + $('#'+settings.marker_hover_id).css('left', pos_x); + var height = marker_hover.height(); + height += parseInt(marker_hover.css("padding-top"), 10) + + parseInt(marker_hover.css("padding-bottom"), 10) + + parseInt(marker_hover.css("margin-top"), 10) + + parseInt(marker_hover.css("margin-bottom"), 10) + + parseInt(marker_hover.css("borderBottomWidth"), 10) + + parseInt(marker_hover.css("borderTopWidth"), 10); + var pos_y = px.y + map_position.top + - height - marker.icon.size.h; + if (settings.marker_hover_offset) + pos_y += settings.marker_hover_offset.y; + $('#'+settings.marker_hover_id).css('top', pos_y); + $('#'+settings.marker_hover_id).show(); + OpenLayers.Event.stop(evt); + }; + var markerOut = function (evt) { + document.body.style.cursor='auto'; + var marker = evt.object; + if (marker.icon_hover_url){ + marker.setUrl(marker.icon_url); + } + $('#'+settings.marker_hover_id).hide(); + OpenLayers.Event.stop(evt); + }; + marker.events.register('click', feature, markerClick); + marker.events.register('mouseover', feature, markerOver); + marker.events.register('mouseout', feature, markerOut); + settings.layerMarkers.addMarker(marker); + /* show the item when designed in the permalink */ + if (settings.display_feature == feature.pk){ + settings.current_feature = feature; + _popup(feature); + methods.display_feature_detail(feature.pk); + if (!settings.display_route){ + settings.map.setCenter(feature.lonlat, 16); + _repan_popup(); + } + settings.display_feature = null; + //methods.loadCategories(); + } + + if (settings.enable_clustering){ + // manage cluster layer + var point = new OpenLayers.Geometry.Point(lon, lat).transform( + EPSG_DISPLAY_PROJECTION, + EPSG_PROJECTION); + var feat = new OpenLayers.Feature.Vector(point); + feat.attributes = { icon: MEDIA_URL + mark.properties.icon_path, + name: "", label:"", pk:mark.properties.pk, + marker:marker}; + settings.cluster_array.push(feat); + } + + return feature; + }, + + /* + * Put a route on the map + */ + addRoute: function(route) { + var polyline = route.geometry; + var point_array = new Array(); + for (i=0; i 0){ + uri += '_'; + } + uri += step.lon + '_' + step.lat; + } + settings.permalink.updateLink(); + $.ajax({url: uri, + dataType: "json", + success: function (data) { + settings.layerRoute.removeAllFeatures(); + methods.redrawRoutingIcons(); + methods.hideMessage(); + if (!data.features.length){ + methods.displayMessage(routing_fail_message); + return; + } + for (var i = 0; i < data.features.length; i++) { + var feat = data.features[i]; + if(feat.type == 'LineString'){ + settings.current_route_feature = + methods.putRoute(feat); + } else { + var lonlat = new OpenLayers.LonLat( + feat.geometry.coordinates[0], + feat.geometry.coordinates[1]); + lonlat.transform(EPSG_DISPLAY_PROJECTION, + settings.map.getProjectionObject()); + var icon_height = feat.properties.icon_height; + var icon_width = feat.properties.icon_width; + var marker = new OpenLayers.Marker(lonlat, + new OpenLayers.Icon( + feat.properties.icon_path, + new OpenLayers.Size(icon_width, + icon_height), + new OpenLayers.Pixel( + -(icon_width/2), + -icon_height)) + ); + settings.layerRouteMarker.addMarker(marker); + } + } + if (data.message) methods.displayMessage(data.message); + settings.map.zoomToExtent( + settings.layerRoute.getDataExtent()); + settings.map.zoomOut(); + $('#id_transport_it').find('span' + ).removeClass('selected'); + $('#id_transport_it_'+data.properties.transport + ).addClass('selected'); + $('#chimere_total_label').html( + data.properties.total); + $('#chimere_itinerary_content').html( + data.properties.description); + $('#chimere_itinerary').show(); + if(!settings.edition_type_is_route){ + $('#chimere_itinerary_form').hide(); + settings.routing_panel_open(); + } else { + methods.updateRoutingInput(); + } + }, + error: function (jqXHR, textStatus, errorThrown) { + methods.redrawRoutingIcons(); + methods.hideMessage(); + console.log(errorThrown); + console.log(textStatus); + settings.layerRoute.removeAllFeatures(); + methods.displayMessage(routing_fail_message); + } + }); + + }, + /* + Put a route on the map + */ + putRoute: function(polyline) { + var point_array = new Array(); + for (i=0; i" + data + ""); + } + } + } + }); + }, + displayMessage: function(message){ + if (!$('#chimere_message').length) return; + $('#chimere_message').html(message); + $('#chimere_message').dialog('open'); + }, + hideMessage: function(message){ + if (!$('#chimere_message').length) return; + $('#chimere_message').dialog('close'); + }, + center_on_feature: function(feature) { + var f = get_or_set(feature, settings.current_feature); + if (f) + { + settings.map.setCenter(f.lonlat); + } + }, + zoom: function (options) { + if ($.hasattr("category", options)) { + helpers.zoom_to_category(options["category"]); + } else if ($.hasattr("subcategories", options)) { + helpers.zoom_to_subcategories(options["subcategories"]); + } else if ($.hasattr("area", options)) { + helpers.zoom_to_area(options["area"]); + } + }, + category_detail: function (category_id) { + /* show the detail of a category */ + var uri = extra_url + "getDescriptionDetail/" + category_id; + $.ajax({url:uri, + success: function (data) { + $("#category_description").html(data).dialog(); + $("#category_description").dialog( "option", "title", + $("#category_title").html()); + } + }); + }, + /* + * Load the subcategory description if available + */ + subcategory_detail: function(category_id){ + var uri = extra_url + "getCategory/" + category_id; + + $.ajax({url: uri, + dataType: "json", + success: function (data) { + if (!data.description){return} + $('#category_description').html(data.description); + $("#category_description").dialog("option", "title", + data.name); + $('#category_description').dialog('open'); + }, + error: function (data) { + // fail silently + } + }); + }, + toggle_category: function (id) { + // TODO make this id DOM element customisable + // Check if element is currently visible or not + var was_visible = $("#maincategory_" + id).is(":visible"); + // Close all categories + var category_plus = STATIC_URL + "chimere/img/plus.png"; + var category_minus = STATIC_URL + "chimere/img/minus.png"; + if (settings.category_accordion){ + $("#categories ul.subcategories").hide(); + $("#categories img.toggle_category").attr("src", category_plus); + $("#categories .main_category").addClass("toggle_plus"); + $("#categories .main_category").removeClass("toggle_minus"); + } + // Put a minus image + if (!was_visible) + { + // Show the subcategories + $("#maincategory_" + id).toggle(); + $("#maincategory_" + id).parent().addClass("toggle_minus"); + $("#maincategory_" + id).parent().removeClass("toggle_plus"); + // Put a plus image + $("#maincategory_img_" + id).attr("src", category_minus); + settings.current_category = id; + } + if (!settings.category_accordion && was_visible) + { + $("#maincategory_" + id).toggle(); + $("#maincategory_" + id).parent().addClass("toggle_plus"); + $("#maincategory_" + id).parent().removeClass("toggle_minus"); + // Put a minus image + $("#maincategory_img_" + id).attr("src", category_plus); + } + }, + zoomToCurrentExtent: function(){ + /* zoom to current extent */ + var current_extent = helpers.getSavedExtent(); + var extent; + if (OpenLayers && current_extent && current_extent.length == 4){ + extent = new OpenLayers.Bounds( + current_extent[0], current_extent[1], + current_extent[2], current_extent[3]); + } + /* + else if (OpenLayers && default_area && default_area.length == 4){ + extent = new OpenLayers.Bounds(default_area[0], default_area[1], + default_area[2], default_area[3]); + }*/ + else{ + return; + } + extent.transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); + settings.map.zoomToExtent(extent, true); + return true; + }, + // methods for edition + setMarker: function (event){ + event = event || window.event; + var lonlat = settings.map.getLonLatFromViewPortPx(event.xy); + methods.putEditMarker(lonlat, false); + OpenLayers.Event.stop(event); + }, + /* put the marker on the map and update latitude and longitude fields */ + putEditMarker: function (lonlat, zoom){ + if (settings.current_edit_feature) { + settings.layerMarkers.removeMarker(settings.current_edit_feature); + } + settings.current_edit_feature = new OpenLayers.Marker(lonlat.clone(), + settings.default_icon); + settings.layerMarkers.addMarker(settings.current_edit_feature); + methods.updateMarkerInput(); + /* zoom to the point */ + if (zoom){ + var bounds = settings.layerMarkers.getDataExtent(); + if (bounds) settings.map.zoomToExtent(bounds); + } + return; + }, + updateMarkerInput: function(){ + if (!settings.current_edit_feature) { + return; + } + lonlat = settings.current_edit_feature.lonlat.clone().transform( + settings.map.getProjectionObject(), + EPSG_DISPLAY_PROJECTION); + $('#id_point').val('POINT(' + lonlat.lon + ' ' + lonlat.lat + ')'); + if($('#live_latitude').length){ + $('#live_latitude').val(lonlat.lat); + $('#live_longitude').val(lonlat.lon); + } + + }, + updateRoutingInput: function(){ + if (!settings.current_route_feature) { + return; + } + var current_geo = settings.current_route_feature.geometry.clone(); + current_geo.transform(EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); + jQuery('#id_route').val(current_geo); + }, + activateCurrentControl: function(){ + if (settings.current_control){ + settings.current_control.activate(); + } else { + var pathCreate = settings.map.getControlsByClass( + 'OpenLayers.Control.DrawFeature'); + if (pathCreate){ + pathCreate[0].activate(); + } + } + var pathModify = settings.map.getControlsByClass( + 'OpenLayers.Control.ModifyFeature'); + if (settings.current_feature && pathModify){ + pathModify[0].selectControl.select(settings.current_feature); + } + }, + deactivateCurrentControl: function(){ + if (settings.current_control){ + settings.current_control.deactivate(); + } + }, + initFeature: function(json_geometry){ + var json = new OpenLayers.Format.JSON(); + var polyline = json.read(json_geometry); + var point_array = new Array(); + for (i=0; i - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -See the file COPYING for details. -*/ - -/* Add OpenLayers MapQuest layer management */ -OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { - name: "MapQuestOSM", - sphericalMercator: true, - url: ' http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png', - clone: function(obj) { - if (obj == null) { - obj = new OpenLayers.Layer.OSM( - this.name, this.url, this.getOptions()); - } - obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); - return obj; - }, - CLASS_NAME: "OpenLayers.Layer.MapQuestOSM" -}); - -/* -* Little hasattr helper -*/ -(function ($) { - $.hasattr = function (key, arr) { - var v = arr[key]; - if (typeof v === "undefined") - return false; - else - return v; }; -})( jQuery ); - -(function ($) { - /* - * Chimere jQuery plugin - */ - /* - * Default settings - */ - var defaults = { - restricted_extent: false, - permalink_label: null, - permalink_div: null, - permalink: null, // OL Control, could be overrided - map_layers: null, - selected_map_layer: null, - dynamic_categories: false, - display_submited: false, - display_feature: null, - display_route: null, - area_id: null, - checked_categories: [], - zoom: null, - lat: null, - lon: null, - simple: false, - routing_start_lat: null, - routing_start_lon: null, - routing_end_lat: null, - routing_end_lon: null, - routing_steps_lonlat: null, - // Provide this function to make a custom click event on the marker - on_marker_click: null, - // Provide this function to override the feature detail display - display_feature_detail_fx: null, - // Provide this function for overriding the getSubcategories default - get_subcategories_fx: null, - hide_popup_fx: null, - // if leave to false every click on the map hide the pop-up - explicit_popup_hide: false, - controls:[new OpenLayers.Control.Navigation(), - new OpenLayers.Control.PanPanel(), - new OpenLayers.Control.ZoomPanel(), - new OpenLayers.Control.ScaleLine()], - popupClass: OpenLayers.Popup.FramedCloud, - popupContentFull: false, // if true the detail is inside the popup - category_accordion: true, // category opening behave like an accordion - maxResolution: 156543.0399, - units: 'm', - projection: new OpenLayers.Projection('EPSG:4326'), - theme: null, - enable_clustering: false, - routing: false, // enable routing management - routing_panel_open: function(){ - $('#chimere_itinerary_panel').dialog('open'); - }, - current_feature: null, // To store the active POI - current_control: null, // To store the current control - current_popup: null, // To store the current POI popup displayed - current_category: null, // To store the current category clicked in list - current_route_feature: null, // To store the current route find by routing - itinerary_step_number:0, // current step number - icon_offset: new OpenLayers.Pixel(0, 0), - edition: false, // edition mode - edition_type_is_route: false, // route or POI edition - default_icon: new OpenLayers.Icon( - 'http://www.openlayers.org/dev/img/marker-green.png', - new OpenLayers.Size(21, 25), - new OpenLayers.Pixel(-(21/2), -25)), - cluster_icon: null, - marker_hover_id:'marker_hover', - marker_hover_content_id:'marker_hover_content', - marker_hover_offset: null, - icon_start: null, - icon_step: null, - icon_end: null - }; - var settings = {}; - /* - * Publics methods - */ - var methods = { - /* - * Plugin init function - */ - init: function ( options ) { - /* Manage parameters */ - // not staticaly in default because of STATIC_URL init - if (defaults.cluster_icon == null && typeof STATIC_URL != 'undefined'){ - defaults.cluster_icon = new OpenLayers.Icon( - STATIC_URL+'chimere/img/marker-cluster.png', - new OpenLayers.Size(36, 39), - new OpenLayers.Pixel(-(36/2), -(39/2))); - } - if (defaults.icon_start == null && typeof STATIC_URL != 'undefined'){ - defaults.icon_start = new OpenLayers.Icon( - STATIC_URL + "chimere/img/flag-start.png", - new OpenLayers.Size(32, 32), - new OpenLayers.Pixel(0, -32)); - } - if (defaults.icon_step == null && typeof STATIC_URL != 'undefined'){ - defaults.icon_step = new OpenLayers.Icon( - STATIC_URL + "chimere/img/flag-step.png", - new OpenLayers.Size(32, 32), - new OpenLayers.Pixel(0, -32)); - } - if (defaults.icon_end == null && typeof STATIC_URL != 'undefined'){ - defaults.icon_end = new OpenLayers.Icon( - STATIC_URL + "chimere/img/flag-finish.png", - new OpenLayers.Size(32, 32), - new OpenLayers.Pixel(0, -32)); - } - settings = $.extend({}, defaults); - if ( options ) $.extend(settings, options); - var map_element = $(this).get(0); - var map_options = { - controls: settings.controls, - maxResolution: settings.maxResolution, - units: settings.units, - projection: settings.projection, - theme: settings.theme - }; - if (settings.restricted_extent){ - settings.restricted_extent.transform(EPSG_DISPLAY_PROJECTION, - EPSG_PROJECTION); - map_options['restrictedExtent'] = settings.restricted_extent; - } - - settings.current_position = null; - - /* Create map object */ - settings.map = map = new OpenLayers.Map(map_element, map_options); - - map.addControl(new OpenLayers.Control.Attribution()); - - /* Manage permalink */ - if (!settings.edition){ - if (settings.permalink == null && !settings.edition) { - var permalink_options = {}; - if (settings.permalink_element){ - // hard to understand from OL documentation... - permalink_options["div"] = settings.permalink_element; - } - settings.permalink = new OpenLayers.Control.Permalink( - permalink_options); - } - /* HACK new permalink createParams method - FIXME when facilities are given to personalize the permalink */ - var oldCreateParams = settings.permalink.createParams - var _createParams = function(center, zoom, layers) { - // Call normal method - var params = oldCreateParams(center, zoom, layers); - // Make specific params - params.checked_categories = settings.checked_categories; - params.display_submited = settings.display_submited; - if(settings.current_feature) - params.current_feature = settings.current_feature.pk; - if (settings.routing_start){ - lonlat = settings.routing_start.lonlat.clone().transform( - EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); - params.routing_start_lon = lonlat.lon; - params.routing_start_lat = lonlat.lat; - } - if (settings.routing_end){ - lonlat = settings.routing_end.lonlat.clone().transform( - EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); - params.routing_end_lon = lonlat.lon; - params.routing_end_lat = lonlat.lat; - } - if (settings.routing_steps){ - var steps = []; - for (var i = 0; i < settings.routing_steps.length; i++){ - lonlat = settings.routing_steps[i].lonlat.clone( - ).transform(EPSG_PROJECTION, - EPSG_DISPLAY_PROJECTION); - steps.push([lonlat.lon, lonlat.lat]); - } - params.routing_steps = steps; - } - return params; - } - // Force new createParams method - settings.permalink.createParams = _createParams; - settings.map.addControl(settings.permalink); - // update with the translated permalink label - if(settings.permalink_label && settings.permalink.div - && settings.permalink.div.childNodes.length > 0){ - settings.permalink.div.childNodes[0].textContent = settings.permalink_label; - } - } - /* Add Layers */ - settings.map.addLayers(settings.map_layers); - if (settings.map_layers.length > 1){ - settings.map.addControl(new OpenLayers.Control.LayerSwitcher( - {roundedCorner:false})); - } - /* select the default map layer */ - if (!settings.selected_map_layer){ - settings.selected_map_layer = 0; - } - settings.map.setBaseLayer( - settings.map_layers[settings.selected_map_layer]); - - /* manage the context menu */ - $('#map_menu_zoomin').bind("click", methods.zoomIn); - $('#map_menu_zoomout').bind("click", methods.zoomOut); - $('#map_menu_center').bind("click", methods.mapCenter); - /* manage the routing */ - if (settings.routing){ - settings.routing_start = null; - settings.routing_steps = new Array(); - settings.routing_end = null; - $('#map_menu_from').bind("click", methods.routingFrom); - $('#map_menu_step').bind("click", methods.routingAddStep); - $('#map_menu_to').bind("click", methods.routingTo); - $('#map_menu_clear').bind("click", methods.routingClear); - settings.layerRoute = new OpenLayers.Layer.Vector("Route Layer"); - settings.map.addLayer(settings.layerRoute); - settings.layerRouteMarker = new OpenLayers.Layer.Markers( - 'Route markers'); - settings.map.addLayer(settings.layerRouteMarker); - } - /* Vectors layer */ - settings.layerVectors = new OpenLayers.Layer.Vector("Vector Layer"); - settings.map.addLayer(settings.layerVectors); - settings.layerVectors.setOpacity(0.8); - if (settings.edition_type_is_route){ - settings.map.addControl(new OpenLayers.Control.DrawFeature( - settings.layerVectors, OpenLayers.Handler.Path)); - settings.map.addControl(new OpenLayers.Control.ModifyFeature( - settings.layerVectors, {clickout:false, toggle:false})); - } - - if (settings.enable_clustering){ - var style = new OpenLayers.Style({ - graphicTitle: "${name}", - externalGraphic: "${icon}", - graphicWidth: "${width}", - graphicHeight: "${height}", - graphicXOffset: "${offsetx}", - graphicYOffset: "${offsety}", - graphicOpacity: 1, - label: "${label}", - labelYOffset: "2", - fontSize:'1.3em' - }, { - context: { - name: function(feature) { - if(feature.cluster) { - feature.attributes.width = settings.cluster_icon.size.w; - feature.attributes.height = settings.cluster_icon.size.h; - feature.attributes.offsetx = settings.cluster_icon.offset.x; - feature.attributes.offsety = settings.cluster_icon.offset.y; - } else{ - var marker = feature.attributes.marker - feature.attributes.width = marker.icon.size.w; - feature.attributes.height = marker.icon.size.h; - feature.attributes.offsetx = settings.icon_offset.x; - feature.attributes.offsety = settings.icon_offset.y; - } - return feature.attributes.name; - }, - label: function(feature) { - // clustered features count or blank if feature is not a cluster - return feature.cluster ? feature.cluster.length : ""; - }, - icon: function(feature) { - if (feature.cluster){ - return settings.cluster_icon.url; - } else { - return STATIC_URL + 'chimere/img/empty.png'; - } - }, - width: function(feature) { return feature.attributes.width; }, - height: function(feature) { return feature.attributes.height; }, - offsetx: function(feature) { return feature.attributes.offsetx; }, - offsety: function(feature) { return feature.attributes.offsety; } - }}); - - - /* Cluster layer */ - settings.clustering = new OpenLayers.Strategy.Cluster({ - distance: 10, - threshold: 2}); - settings.layerCluster = new OpenLayers.Layer.Vector("Cluster layer", - {styleMap: new OpenLayers.StyleMap({'default': style}), - strategies: [settings.clustering]}); - settings.map.addLayer(settings.layerCluster); - - var highlightCtrl = new OpenLayers.Control.SelectFeature( - settings.layerCluster, { - hover: true, - highlightOnly: true, - eventListeners: { - featurehighlighted: function(e) { - if(e.feature.attributes.marker) - e.feature.attributes.marker.events.triggerEvent('mouseover'); - }, - featureunhighlighted: function(e) { - if(e.feature.attributes.marker) - e.feature.attributes.marker.events.triggerEvent('mouseout'); - } - } - }); - - var selectCtrl = new OpenLayers.Control.SelectFeature( - settings.layerCluster,{ - onSelect: methods.zoomOnCluster - }); - - settings.map.addControl(highlightCtrl); - settings.map.addControl(selectCtrl); - - highlightCtrl.activate(); - selectCtrl.activate(); - } - - /* Markers layer */ - settings.layerMarkers = new OpenLayers.Layer.Markers('POIs'); - settings.map.addLayer(settings.layerMarkers); - - if (settings.dynamic_categories){ - settings.map.events.register('moveend', settings.map, - methods.loadCategories); - } - /* TODO make a function */ - if (settings.display_submited) { - document.getElementById('display_submited_check').checked = true; - } - /* if we have some zoom and lon/lat from the init options */ - if (settings.zoom && settings.lon && settings.lat) { - var centerLonLat = new OpenLayers.LonLat(settings.lon, - settings.lat); - settings.map.setCenter(centerLonLat, settings.zoom); - } - /* if not zoom to the extent in cookies */ - else if (!methods.zoomToCurrentExtent(settings.map)){ - /* if no extent in cookies zoom to default */ - if(CENTER_LONLAT && DEFAULT_ZOOM){ - settings.map.setCenter(CENTER_LONLAT, DEFAULT_ZOOM); - } - } - - if (!settings.edition){ - if (settings.enable_clustering){ - settings.map.events.register('zoomend', null, - methods.cleanCluster); - } - methods.loadCategories(); - methods.loadGeoObjects(); - methods.activateContextMenu() - } else { - if (!settings.edition_type_is_route){ - methods.activateMarkerEdit(); - } else { - settings.layerVectors.events.register('featuremodified', - settings.layerVectors, helpers.updateRouteForm); - settings.layerVectors.events.register('featureadded', - settings.layerVectors, helpers.featureRouteCreated); - } - } - if (settings.routing_start_lon && settings.routing_start_lat){ - settings.current_position = new OpenLayers.LonLat( - settings.routing_start_lon, settings.routing_start_lat - ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); - methods.routingFrom(); - } - if (settings.routing_end_lon && settings.routing_end_lat){ - settings.current_position = new OpenLayers.LonLat( - settings.routing_end_lon, settings.routing_end_lat - ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); - methods.routingTo(); - } - if (settings.routing_steps_lonlat){ - for (var i = 0; i < settings.routing_steps_lonlat.length/2; i++) { - lon = settings.routing_steps_lonlat[i*2]; - lat = settings.routing_steps_lonlat[i*2+1]; - settings.current_position = new OpenLayers.LonLat(lon, lat - ).transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); - methods.routingAddStep(); - } - } - methods.preload_images(); - }, // end of init - /* Preload icons */ - preload_images: function(){ - if (typeof extra_url == 'undefined') return; - var uri = extra_url + "getAllCategories/"; - $.ajax({url: uri, - dataType: "json", - success: function (data) { - if (!data.categories){return} - for(var idx=0; idx span"); - if (parent.find('input[type=checkbox]:checked').length){ - parent_label.addClass('category-selected'); - } else { - parent_label.removeClass('category-selected'); - } - var master_check = parent.find("> input"); - if (parent.find('.subcategories input[type=checkbox]').length == - parent.find('.subcategories input[type=checkbox]:checked').length){ - master_check.attr('checked', 'checked'); - } else { - master_check.removeAttr('checked'); - } - - if($('#action-categories').length){ - if ($('#categories input[type=checkbox]:checked').length){ - $('#action-categories').addClass('category-selected'); - } else { - $('#action-categories').removeClass('category-selected'); - } - } - return master_check; - }; - var _init_categories = function () { - /* - * Add event listener in categories DOM elements - */ - $('#categories #ul_categories > li > input').bind("click", - function (e) { - methods.hidePopup(e); - _toggle_subcategories($(this)); - methods.loadGeoObjects(); - settings.permalink.updateLink(); - }); - $('.subcategories li input').bind("click", function (e) { - var c_name = $(this).attr('name'); - c_name = c_name.substr(c_name.lastIndexOf("_")+1); - if($(this).is(':checked')){ - methods.subcategory_detail(c_name); - } - var par = $(this).parent(); - if ($(this).attr('checked')){ - par.addClass('selected'); - } else { - par.removeClass('selected'); - } - methods.hidePopup(e); - methods.loadGeoObjects(); - _toggle_categories($(this)); - settings.permalink.updateLink(); - if ($('#layer_cat_'+c_name).length){ - $('#layer_cat_'+c_name).prop("checked", - this.checked); - } - }); - $('#display_submited_check').bind("click", function () { - methods.loadGeoObjects(); - settings.permalink.updateLink(); - }); - // Zoom to category - $(".zoom_to_category").bind("click", function (e) { - var id = this.id.substr(this.id.lastIndexOf("_")+1); - helpers.zoom_to_category(id); - }); - $(".zoom_to_subcategory").bind("click", function (e) { - var id = this.id.substr(this.id.lastIndexOf("_")+1); - helpers.zoom_to_subcategories([id]); - }); - $(".toggle_category").parent().bind("click", function (e) { - var item = $(this).children('.toggle_category'); - var id = item.attr('id').substr(item.attr('id').lastIndexOf("_")+1); - methods.toggle_category(id); - }); - } - var _reCheckCategories = function (){ - /* recheck categories on init or when a redraw occurs */ - if (!settings.checked_categories){ - return; - } - $('#frm_categories .subcategories input:checkbox').each(function(index){ - cat_id = $(this).attr('id').split('_').pop(); - if (settings.checked_categories.indexOf(parseInt(cat_id)) != -1) { - $(this).attr("checked", "checked"); - _toggle_categories($(this)); - methods.toggle_category(); - } else { - $(this).attr("checked", false); - } - }); - if (settings.display_submited == true){ - $('#display_submited_check').attr("checked", "checked"); - } - } - }, - /* - * Hide clusterized markers - */ - cleanCluster: function (){ - if (settings.map.getZoom() === 18) { - // Don't cluster at this level. No matter what. - settings.clustering.threshold = 1000; - } else { - settings.clustering.threshold = 2; - } - //settings.layerCluster.refresh({force:true}); - settings.clustering.recluster(); - var hidden_feature_idx = []; - if (settings.map.getZoom() != 18) { - for(var idx=0; idx -1){ - settings.layerMarkers.markers[j].display(false); - } else { - settings.layerMarkers.markers[j].display(true); - } - } - }, - /* - * Put a marker on the map - */ - addMarker: function (mark) { - /* - * Default Feature configuration - * This can be overrided in on_marker_click, using settings.current_feature - */ - var lat = mark.geometry.coordinates[1]; - var lon = mark.geometry.coordinates[0]; - var size = new OpenLayers.Size(mark.properties.icon_width, - mark.properties.icon_height); - var icon_url = MEDIA_URL + mark.properties.icon_path; - var icon_hover_url = ''; - if (mark.properties.icon_hover_path){ - var icon_hover_url = MEDIA_URL + mark.properties.icon_hover_path; - } - var iconclone = new OpenLayers.Icon(icon_url, size, - settings.icon_offset); - - var feature = new OpenLayers.Feature(settings.layerMarkers, - new OpenLayers.LonLat(lon, lat).transform( - EPSG_DISPLAY_PROJECTION, - EPSG_PROJECTION), - {icon:iconclone}); - feature.pk = mark.properties.pk; - feature.popupClass = settings.popupClass; - feature.data.popupContentHTML = "
"; - if (!settings.popupContentFull) { - feature.data.popupContentHTML += mark.properties.name; - } - feature.data.popupContentHTML += "
"; - feature.data.overflow = 'hidden'; - var marker = feature.createMarker(); - marker.pk = feature.pk; - marker.icon_url = icon_url; - marker.icon_hover_url = icon_hover_url; - marker.category_name = mark.properties.category_name; - /* manage markers events */ - var _popup = function() { - /* show the popup */ - if (settings.current_popup != null) { - settings.current_popup.hide(); - } - if (feature.popup == null) { - feature.popup = feature.createPopup(); - settings.map.addPopup(feature.popup); - } else { - feature.popup.toggle(); - } - settings.current_popup = feature.popup; - /* hide on click on the cloud */ - if (!settings.explicit_popup_hide){ - settings.current_popup.groupDiv.onclick = methods.hidePopup; - } - settings.permalink.updateLink(); - } - var _repan_popup = function(){ - /* re-pan manually */ - - // no clean way to detect if all the element are ready - // lack of better... - setTimeout( - function(){ - settings.current_popup.panIntoView(); - }, 1000); - } - - var markerClick = function (evt) { - settings.current_feature = feature; - methods.setCurrentPosition(feature.lonlat); - if ( settings.on_marker_click ) { - settings.on_marker_click(evt, mark, settings); - } - else - { - methods.center_on_feature(); - $('#chimere_map_menu').hide(); - // Default popup - if (feature.popup && feature.popup.visible()) { - if (settings.current_popup == feature.popup) { - feature.popup.hide(); - if (!settings.simple){ - $('#detail').hide(); - } - } else { - settings.current_popup.hide(); - _popup(); - methods.display_feature_detail(feature.pk); - _repan_popup(); - } - } else { - _popup(); - methods.display_feature_detail(feature.pk); - _repan_popup(); - } - } - OpenLayers.Event.stop(evt); - }; - var markerOver = function (evt) { - document.body.style.cursor='pointer'; - if (settings.current_feature && settings.current_feature.popup - && settings.current_feature.popup.visible()) return; - var marker = evt.object; - if (marker.icon_hover_url){ - marker.setUrl(marker.icon_hover_url); - } - px = settings.map.getPixelFromLonLat(marker.lonlat); - marker_hover = $('#'+settings.marker_hover_id); - marker_hover_content = $('#'+settings.marker_hover_content_id); - marker_hover_content.html(marker.category_name); - var map_position = $(settings.map.div).offset(); - - var width = marker_hover.width(); - width += parseInt(marker_hover.css("padding-left"), 10) - + parseInt(marker_hover.css("padding-right"), 10) - + parseInt(marker_hover.css("margin-left"), 10) - + parseInt(marker_hover.css("margin-right"), 10) - + parseInt(marker_hover.css("borderLeftWidth"), 10) - + parseInt(marker_hover.css("borderRightWidth"), 10); - var pos_x = px.x + map_position.left - - width/2 + 1; - if (settings.marker_hover_offset) - pos_x += settings.marker_hover_offset.x; - $('#'+settings.marker_hover_id).css('left', pos_x); - var height = marker_hover.height(); - height += parseInt(marker_hover.css("padding-top"), 10) - + parseInt(marker_hover.css("padding-bottom"), 10) - + parseInt(marker_hover.css("margin-top"), 10) - + parseInt(marker_hover.css("margin-bottom"), 10) - + parseInt(marker_hover.css("borderBottomWidth"), 10) - + parseInt(marker_hover.css("borderTopWidth"), 10); - var pos_y = px.y + map_position.top - - height - marker.icon.size.h; - if (settings.marker_hover_offset) - pos_y += settings.marker_hover_offset.y; - $('#'+settings.marker_hover_id).css('top', pos_y); - $('#'+settings.marker_hover_id).show(); - OpenLayers.Event.stop(evt); - }; - var markerOut = function (evt) { - document.body.style.cursor='auto'; - var marker = evt.object; - if (marker.icon_hover_url){ - marker.setUrl(marker.icon_url); - } - $('#'+settings.marker_hover_id).hide(); - OpenLayers.Event.stop(evt); - }; - marker.events.register('click', feature, markerClick); - marker.events.register('mouseover', feature, markerOver); - marker.events.register('mouseout', feature, markerOut); - settings.layerMarkers.addMarker(marker); - /* show the item when designed in the permalink */ - if (settings.display_feature == feature.pk){ - settings.current_feature = feature; - _popup(feature); - methods.display_feature_detail(feature.pk); - if (!settings.display_route){ - settings.map.setCenter(feature.lonlat, 16); - _repan_popup(); - } - settings.display_feature = null; - //methods.loadCategories(); - } - - if (settings.enable_clustering){ - // manage cluster layer - var point = new OpenLayers.Geometry.Point(lon, lat).transform( - EPSG_DISPLAY_PROJECTION, - EPSG_PROJECTION); - var feat = new OpenLayers.Feature.Vector(point); - feat.attributes = { icon: MEDIA_URL + mark.properties.icon_path, - name: "", label:"", pk:mark.properties.pk, - marker:marker}; - settings.cluster_array.push(feat); - } - - return feature; - }, - - /* - * Put a route on the map - */ - addRoute: function(route) { - var polyline = route.geometry; - var point_array = new Array(); - for (i=0; i 0){ - uri += '_'; - } - uri += step.lon + '_' + step.lat; - } - settings.permalink.updateLink(); - $.ajax({url: uri, - dataType: "json", - success: function (data) { - settings.layerRoute.removeAllFeatures(); - methods.redrawRoutingIcons(); - methods.hideMessage(); - if (!data.features.length){ - methods.displayMessage(routing_fail_message); - return; - } - for (var i = 0; i < data.features.length; i++) { - var feat = data.features[i]; - if(feat.type == 'LineString'){ - settings.current_route_feature = - methods.putRoute(feat); - } else { - var lonlat = new OpenLayers.LonLat( - feat.geometry.coordinates[0], - feat.geometry.coordinates[1]); - lonlat.transform(EPSG_DISPLAY_PROJECTION, - settings.map.getProjectionObject()); - var icon_height = feat.properties.icon_height; - var icon_width = feat.properties.icon_width; - var marker = new OpenLayers.Marker(lonlat, - new OpenLayers.Icon( - feat.properties.icon_path, - new OpenLayers.Size(icon_width, - icon_height), - new OpenLayers.Pixel( - -(icon_width/2), - -icon_height)) - ); - settings.layerRouteMarker.addMarker(marker); - } - } - if (data.message) methods.displayMessage(data.message); - settings.map.zoomToExtent( - settings.layerRoute.getDataExtent()); - settings.map.zoomOut(); - $('#id_transport_it').find('span' - ).removeClass('selected'); - $('#id_transport_it_'+data.properties.transport - ).addClass('selected'); - $('#chimere_total_label').html( - data.properties.total); - $('#chimere_itinerary_content').html( - data.properties.description); - $('#chimere_itinerary').show(); - if(!settings.edition_type_is_route){ - $('#chimere_itinerary_form').hide(); - settings.routing_panel_open(); - } else { - methods.updateRoutingInput(); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - methods.redrawRoutingIcons(); - methods.hideMessage(); - console.log(errorThrown); - console.log(textStatus); - settings.layerRoute.removeAllFeatures(); - methods.displayMessage(routing_fail_message); - } - }); - - }, - /* - Put a route on the map - */ - putRoute: function(polyline) { - var point_array = new Array(); - for (i=0; i" + data + ""); - } - } - } - }); - }, - displayMessage: function(message){ - if (!$('#chimere_message').length) return; - $('#chimere_message').html(message); - $('#chimere_message').dialog('open'); - }, - hideMessage: function(message){ - if (!$('#chimere_message').length) return; - $('#chimere_message').dialog('close'); - }, - center_on_feature: function(feature) { - var f = get_or_set(feature, settings.current_feature); - if (f) - { - settings.map.setCenter(f.lonlat); - } - }, - zoom: function (options) { - if ($.hasattr("category", options)) { - helpers.zoom_to_category(options["category"]); - } else if ($.hasattr("subcategories", options)) { - helpers.zoom_to_subcategories(options["subcategories"]); - } else if ($.hasattr("area", options)) { - helpers.zoom_to_area(options["area"]); - } - }, - category_detail: function (category_id) { - /* show the detail of a category */ - var uri = extra_url + "getDescriptionDetail/" + category_id; - $.ajax({url:uri, - success: function (data) { - $("#category_description").html(data).dialog(); - $("#category_description").dialog( "option", "title", - $("#category_title").html()); - } - }); - }, - /* - * Load the subcategory description if available - */ - subcategory_detail: function(category_id){ - var uri = extra_url + "getCategory/" + category_id; - - $.ajax({url: uri, - dataType: "json", - success: function (data) { - if (!data.description){return} - $('#category_description').html(data.description); - $("#category_description").dialog("option", "title", - data.name); - $('#category_description').dialog('open'); - }, - error: function (data) { - // fail silently - } - }); - }, - toggle_category: function (id) { - // TODO make this id DOM element customisable - // Check if element is currently visible or not - var was_visible = $("#maincategory_" + id).is(":visible"); - // Close all categories - var category_plus = STATIC_URL + "chimere/img/plus.png"; - var category_minus = STATIC_URL + "chimere/img/minus.png"; - if (settings.category_accordion){ - $("#categories ul.subcategories").hide(); - $("#categories img.toggle_category").attr("src", category_plus); - $("#categories .main_category").addClass("toggle_plus"); - $("#categories .main_category").removeClass("toggle_minus"); - } - // Put a minus image - if (!was_visible) - { - // Show the subcategories - $("#maincategory_" + id).toggle(); - $("#maincategory_" + id).parent().addClass("toggle_minus"); - $("#maincategory_" + id).parent().removeClass("toggle_plus"); - // Put a plus image - $("#maincategory_img_" + id).attr("src", category_minus); - settings.current_category = id; - } - if (!settings.category_accordion && was_visible) - { - $("#maincategory_" + id).toggle(); - $("#maincategory_" + id).parent().addClass("toggle_plus"); - $("#maincategory_" + id).parent().removeClass("toggle_minus"); - // Put a minus image - $("#maincategory_img_" + id).attr("src", category_plus); - } - }, - zoomToCurrentExtent: function(){ - /* zoom to current extent */ - var current_extent = helpers.getSavedExtent(); - var extent; - if (OpenLayers && current_extent && current_extent.length == 4){ - extent = new OpenLayers.Bounds( - current_extent[0], current_extent[1], - current_extent[2], current_extent[3]); - } - /* - else if (OpenLayers && default_area && default_area.length == 4){ - extent = new OpenLayers.Bounds(default_area[0], default_area[1], - default_area[2], default_area[3]); - }*/ - else{ - return; - } - extent.transform(EPSG_DISPLAY_PROJECTION, EPSG_PROJECTION); - settings.map.zoomToExtent(extent, true); - return true; - }, - // methods for edition - setMarker: function (event){ - event = event || window.event; - var lonlat = settings.map.getLonLatFromViewPortPx(event.xy); - methods.putEditMarker(lonlat, false); - OpenLayers.Event.stop(event); - }, - /* put the marker on the map and update latitude and longitude fields */ - putEditMarker: function (lonlat, zoom){ - if (settings.current_edit_feature) { - settings.layerMarkers.removeMarker(settings.current_edit_feature); - } - settings.current_edit_feature = new OpenLayers.Marker(lonlat.clone(), - settings.default_icon); - settings.layerMarkers.addMarker(settings.current_edit_feature); - methods.updateMarkerInput(); - /* zoom to the point */ - if (zoom){ - var bounds = settings.layerMarkers.getDataExtent(); - if (bounds) settings.map.zoomToExtent(bounds); - } - return; - }, - updateMarkerInput: function(){ - if (!settings.current_edit_feature) { - return; - } - lonlat = settings.current_edit_feature.lonlat.clone().transform( - settings.map.getProjectionObject(), - EPSG_DISPLAY_PROJECTION); - $('#id_point').val('POINT(' + lonlat.lon + ' ' + lonlat.lat + ')'); - if($('#live_latitude').length){ - $('#live_latitude').val(lonlat.lat); - $('#live_longitude').val(lonlat.lon); - } - - }, - updateRoutingInput: function(){ - if (!settings.current_route_feature) { - return; - } - var current_geo = settings.current_route_feature.geometry.clone(); - current_geo.transform(EPSG_PROJECTION, EPSG_DISPLAY_PROJECTION); - jQuery('#id_route').val(current_geo); - }, - activateCurrentControl: function(){ - if (settings.current_control){ - settings.current_control.activate(); - } else { - var pathCreate = settings.map.getControlsByClass( - 'OpenLayers.Control.DrawFeature'); - if (pathCreate){ - pathCreate[0].activate(); - } - } - var pathModify = settings.map.getControlsByClass( - 'OpenLayers.Control.ModifyFeature'); - if (settings.current_feature && pathModify){ - pathModify[0].selectControl.select(settings.current_feature); - } - }, - deactivateCurrentControl: function(){ - if (settings.current_control){ - settings.current_control.deactivate(); - } - }, - initFeature: function(json_geometry){ - var json = new OpenLayers.Format.JSON(); - var polyline = json.read(json_geometry); - var point_array = new Array(); - for (i=0; i{% endfor %} {% if routing %}{% endif %} - - {{ block.super }} {% endblock %} {% block message_edit %}{% endblock %} diff --git a/chimere/widgets.py b/chimere/widgets.py index f39248c..c2e8832 100644 --- a/chimere/widgets.py +++ b/chimere/widgets.py @@ -252,8 +252,7 @@ class PointChooserWidget(forms.TextInput): ["%schimere/css/forms.css" % settings.STATIC_URL,] } js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ - list(settings.JQUERY_JS_URLS) + \ - ["%schimere/js/jquery.chimere.js" % settings.STATIC_URL] + list(settings.JQUERY_JS_URLS) def render(self, name, value, attrs=None, area_name=''): ''' @@ -347,8 +346,7 @@ class RouteChooserWidget(forms.TextInput): } js = settings.MAP_JS_URLS[settings.CHIMERE_EDIT_RENDERER] + \ list(settings.JQUERY_JS_URLS) + \ - ["%schimere/js/jquery.chimere.js" % settings.STATIC_URL, - "%schimere/js/edit_route_map.js" % settings.STATIC_URL, + ["%schimere/js/edit_route_map.js" % settings.STATIC_URL, "%schimere/js/base.js" % settings.STATIC_URL,] def render(self, name, value, attrs=None, area_name='', routefile_id=None): -- cgit v1.2.3 From a204849cb48d8acdaebeac484235c3a91fba00e6 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 1 Apr 2013 18:52:14 +0200 Subject: Add a basic JS file to manage with leaflet --- .../static/chimere/js/jquery.chimere-leaflet.js | 140 +++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 chimere/static/chimere/js/jquery.chimere-leaflet.js diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js new file mode 100644 index 0000000..75a0fc3 --- /dev/null +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -0,0 +1,140 @@ +/* Copyright (C) 2013 Étienne Loks + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +See the file COPYING for details. +*/ + +/* +* Little hasattr helper +*/ +(function ($) { + $.hasattr = function (key, arr) { + var v = arr[key]; + if (typeof v === "undefined") + return false; + else + return v; }; +})( jQuery ); + +(function ($) { + /* + * Chimere leaflet jQuery plugin + */ + /* + * Default settings + */ + var defaults = { + restricted_extent: false, + permalink_label: null, + permalink_div: null, + permalink: null, // OL Control, could be overrided + map_layers: null, + selected_map_layer: null, + dynamic_categories: false, + display_submited: false, + display_feature: null, + display_route: null, + area_id: null, + checked_categories: [], + zoom: null, + lat: null, + lon: null, + simple: false, + routing_start_lat: null, + routing_start_lon: null, + routing_end_lat: null, + routing_end_lon: null, + routing_steps_lonlat: null, + // Provide this function to make a custom click event on the marker + on_marker_click: null, + // Provide this function to override the feature detail display + display_feature_detail_fx: null, + // Provide this function for overriding the getSubcategories default + get_subcategories_fx: null, + hide_popup_fx: null, + // if leave to false every click on the map hide the pop-up + explicit_popup_hide: false, + popupClass: null, + popupContentFull: false, // if true the detail is inside the popup + category_accordion: true, // category opening behave like an accordion + maxResolution: 156543.0399, + units: 'm', + projection: 4326, + theme: null, + enable_clustering: false, + routing: false, // enable routing management + routing_panel_open: function(){ + $('#chimere_itinerary_panel').dialog('open'); + }, + current_feature: null, // To store the active POI + current_control: null, // To store the current control + current_popup: null, // To store the current POI popup displayed + current_category: null, // To store the current category clicked in list + current_route_feature: null, // To store the current route find by routing + itinerary_step_number:0, // current step number + icon_offset: (0, 0), + edition: false, // edition mode + edition_type_is_route: false, // route or POI edition + default_icon: ('http://www.openlayers.org/dev/img/marker-green.png', + (21, 25), (-(21/2), -25)), + cluster_icon: null, + marker_hover_id:'marker_hover', + marker_hover_content_id:'marker_hover_content', + marker_hover_offset: null, + icon_start: null, + icon_step: null, + icon_end: null + }; + var settings = {}; + /* + * Publics methods + */ + var methods = { + /* + * Plugin init function + */ + init: function ( options ) { + } // end of init + }; // End of public methods + var helpers = { + }; // End of helpers + + $.fn.chimere = function (thing) { + // Method calling logic + if ( methods[thing] ) { + return methods[ thing ].apply( this, Array.prototype.slice.call( arguments, 1 )); + } + else if ( typeof thing === 'object' || ! thing ) { + return methods.init.apply( this, arguments ); + } + else if ( thing === 'settings' ) { + // Use $("#mydiv").chimere("settings", "key", "value") to change settings + // from outside the plugin + if (arguments.length == 3) { + settings[arguments[1]] = arguments[2]; + } + else if (arguments.length == 2) { + return settings[arguments[1]]; + } + else { // Use $("#mydiv").chimere("settings") to know the current settings + return settings; + } + } + else { + $.error( 'Method ' + thing + ' does not exist on jQuery.chimere' ); + } + return this; + }; +})( jQuery ); -- cgit v1.2.3 From e3dd113da11a69f2107e6ac7fc5998626a23a2cc Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Wed, 3 Apr 2013 16:30:13 +0200 Subject: First work on Leaflet script * manage markers from a GeoJson source with custom icon --- chimere/settings.sample.py | 11 +- .../static/chimere/js/jquery.chimere-leaflet.js | 371 +++++++++++++++++++++ chimere/static/chimere/js/jquery.chimere-ol.js | 67 ++-- chimere/templates/chimere/blocks/head_chimere.html | 7 +- chimere/templates/chimere/blocks/map.html | 3 +- chimere/templatetags/chimere_tags.py | 4 +- 6 files changed, 429 insertions(+), 34 deletions(-) diff --git a/chimere/settings.sample.py b/chimere/settings.sample.py index 2e5ad40..391c6e5 100644 --- a/chimere/settings.sample.py +++ b/chimere/settings.sample.py @@ -290,7 +290,8 @@ if 'MAP_JS_URLS' not in globals(): STATIC_URL + "openlayers/OpenLayers.js", STATIC_URL + "openlayers/SimplePanZoom.js", "http://www.openstreetmap.org/openlayers/OpenStreetMap.js", - STATIC_URL + "chimere/js/jquery.chimere-ol.js"], + STATIC_URL + "chimere/js/jquery.chimere-ol.js" + ], 'leaflet':[ STATIC_URL + "leaflet/leaflet.js", STATIC_URL + "chimere/js/jquery.chimere-leaflet.js" @@ -299,11 +300,13 @@ if 'MAP_JS_URLS' not in globals(): if 'MAP_CSS_URLS' not in globals(): global MAP_CSS_URLS + # key: [(url, condition)] MAP_CSS_URLS = { - 'openlayers':["http://www.openlayers.org/api/theme/default/style.css"], + 'openlayers':[("http://www.openlayers.org/api/theme/default/style.css", + None)], 'leaflet':[ - STATIC_URL + "leaflet/leaflet.css", - STATIC_URL + "leaflet/leaflet.ie.css" + (STATIC_URL + "leaflet/leaflet.css", None), + (STATIC_URL + "leaflet/leaflet.ie.css", 'lte IE 8') ] } diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 75a0fc3..569f9f5 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -106,9 +106,380 @@ See the file COPYING for details. * Plugin init function */ init: function ( options ) { + settings = $.extend({}, defaults); + if ( options ) $.extend(settings, options); + var map_element = $(this).attr('id'); + + settings.map = map = L.map(map_element); + for (idx in settings.map_layers){ + map.addLayer(settings.map_layers[idx]); + } + if(settings.zoom && settings.lat && settings.lon){ + map.setView([settings.lat, settings.lon], settings.zoom); + } else { + map.fitWorld(); + } + settings.icons = new Object(); + settings.layerMarkers = L.geoJson(null, { + onEachFeature: function (feature, layer) { + layer.bindPopup(feature.properties.name); + }, + pointToLayer: function (feature, latlng) { + if (feature.properties.weigthed){ + var geojsonMarkerOptions = { + radius: 8, + fillColor: "#ff7800", + color: "#000", + weight: 1, + opacity: 1, + fillOpacity: 0.8 + }; + return L.circleMarker(latlng, geojsonMarkerOptions); + } + var icon_path = MEDIA_URL + feature.properties.icon_path; + if (!settings.icons.hasOwnProperty(icon_path)){ + var icon_offset = null; + if (feature.properties.icon_offset){ + icon_offset = feature.properties.icon_offset; + } else { + icon_offset = [feature.properties.icon_width/2, + feature.properties.icon_height]; + } + var popup_anchor = null; + if (feature.properties.popup_anchor){ + popup_anchor = feature.properties.popup_anchor; + } else { + popup_anchor = [0, + -feature.properties.icon_height]; + } + settings.icons[icon_path] = L.icon({ + iconUrl: icon_path, + iconSize: [feature.properties.icon_width, + feature.properties.icon_height], + iconAnchor: icon_offset, + popupAnchor: popup_anchor + }); + } + return L.marker(latlng, {icon: settings.icons[icon_path]}); + } + }).addTo(map); + settings.layerVectors = L.geoJson().addTo(map); + + methods.loadCategories(); + methods.loadGeoObjects(); + }, + hidePopup: function (evt) { + settings.map.closePopup(); + return true; + }, + /* + * Update the categories div in ajax + */ + loadCategories: function () { + var current_extent = settings.map.getBounds(); + current_extent = current_extent.toBBoxString(); + current_extent = current_extent.split(',').join('_'); + current_extent = current_extent.replace(/\./g, 'D'); + current_extent = current_extent.replace(/-/g, 'M'); + var uri = extra_url + if (settings.area_id) uri += settings.area_id + "/"; + uri += "getAvailableCategories/"; + var params = {"current_extent": current_extent} + if (settings.display_submited) params["status"] = "A_S"; + $.ajax({url: uri, + data: params, + cache: false, + success: function (data) { + $('#categories').empty(); + $('#categories').html(data); + _init_categories(); + _reCheckCategories(); + if (settings.current_category) { + // TODO : add a force mode + // (in case the category is yet visible in HTML...) + methods.toggle_category(); + } + } + }); + var _toggle_subcategories = function (category_element) { + // Check subcategories only if unchecked + var val = category_element.is(":checked") ? true : false; + category_element.parent().find("li input").attr("checked", val); + } + var _toggle_categories = function (subcategory_element) { + var parent = subcategory_element.closest('ul'); + var parent_label = parent.parent().find("> span"); + if (parent.find('input[type=checkbox]:checked').length){ + parent_label.addClass('category-selected'); + } else { + parent_label.removeClass('category-selected'); + } + var master_check = parent.find("> input"); + if (parent.find('.subcategories input[type=checkbox]').length == + parent.find('.subcategories input[type=checkbox]:checked').length){ + master_check.attr('checked', 'checked'); + } else { + master_check.removeAttr('checked'); + } + + if($('#action-categories').length){ + if ($('#categories input[type=checkbox]:checked').length){ + $('#action-categories').addClass('category-selected'); + } else { + $('#action-categories').removeClass('category-selected'); + } + } + return master_check; + }; + var _init_categories = function () { + /* + * Add event listener in categories DOM elements + */ + $('#categories #ul_categories > li > input').bind("click", + function (e) { + methods.hidePopup(e); + _toggle_subcategories($(this)); + methods.loadGeoObjects(); + //settings.permalink.updateLink(); + }); + $('.subcategories li input').bind("click", function (e) { + var c_name = $(this).attr('name'); + c_name = c_name.substr(c_name.lastIndexOf("_")+1); + if($(this).is(':checked')){ + methods.subcategory_detail(c_name); + } + var par = $(this).parent(); + if ($(this).attr('checked')){ + par.addClass('selected'); + } else { + par.removeClass('selected'); + } + methods.hidePopup(e); + methods.loadGeoObjects(); + _toggle_categories($(this)); + //settings.permalink.updateLink(); + if ($('#layer_cat_'+c_name).length){ + $('#layer_cat_'+c_name).prop("checked", + this.checked); + } + }); + $('#display_submited_check').bind("click", function () { + methods.loadGeoObjects(); + //settings.permalink.updateLink(); + }); + // Zoom to category + $(".zoom_to_category").bind("click", function (e) { + var id = this.id.substr(this.id.lastIndexOf("_")+1); + helpers.zoom_to_category(id); + }); + $(".zoom_to_subcategory").bind("click", function (e) { + var id = this.id.substr(this.id.lastIndexOf("_")+1); + helpers.zoom_to_subcategories([id]); + }); + $(".toggle_category").parent().bind("click", function (e) { + var item = $(this).children('.toggle_category'); + var id = item.attr('id').substr(item.attr('id').lastIndexOf("_")+1); + methods.toggle_category(id); + }); + } + var _reCheckCategories = function (){ + /* recheck categories on init or when a redraw occurs */ + if (!settings.checked_categories){ + return; + } + $('#frm_categories .subcategories input:checkbox').each(function(index){ + cat_id = $(this).attr('id').split('_').pop(); + if (settings.checked_categories.indexOf(parseInt(cat_id)) != -1) { + $(this).attr("checked", "checked"); + _toggle_categories($(this)); + methods.toggle_category(); + } else { + $(this).attr("checked", false); + } + }); + if (settings.display_submited == true){ + $('#display_submited_check').attr("checked", "checked"); + } + } + }, + /* + * Load markers and route from DB + */ + loadGeoObjects: function () { + if ($('#waiting').length){ + $('#waiting').show(); + } + helpers.retrieve_checked_categories(); + var ids = settings.checked_categories.join('_'); + if (!ids) ids = '0'; + var uri = extra_url + "getGeoObjects/" + ids; + if (settings.display_submited) uri += "/A_S"; + $.ajax({url: uri, + dataType: "json", + success: function (data) { + settings.layerMarkers.clearLayers(); + settings.layerVectors.clearLayers(); + settings.layerMarkers.addData(data); + /* + for (var i = 0; i < data.features.length; i++) { + var feature = data.features[i]; + if (feature.geometry.type == 'Point'){ + methods.addMarker(feature); + } else if (feature.geometry.type == 'LineString') { + methods.addRoute(feature); + } else if (feature.geometry.type == 'MultiLineString') { + methods.addMultiLine(feature); + } + }*/ + }, + error: function (data, textStatus, errorThrown) { + settings.layerMarkers.clearLayers(); + settings.layerVectors.clearLayers(); + }, + complete: function () { + if($('#waiting').length){$('#waiting').hide();} + } + }); + }, + subcategory_detail: function(category_id){ + var uri = extra_url + "getCategory/" + category_id; + + $.ajax({url: uri, + dataType: "json", + success: function (data) { + if (!data.description){return} + $('#category_description').html(data.description); + $("#category_description").dialog("option", "title", + data.name); + $('#category_description').dialog('open'); + }, + error: function (data) { + // fail silently + } + }); + }, + toggle_category: function (id) { + // TODO make this id DOM element customisable + // Check if element is currently visible or not + var was_visible = $("#maincategory_" + id).is(":visible"); + // Close all categories + var category_plus = STATIC_URL + "chimere/img/plus.png"; + var category_minus = STATIC_URL + "chimere/img/minus.png"; + if (settings.category_accordion){ + $("#categories ul.subcategories").hide(); + $("#categories img.toggle_category").attr("src", category_plus); + $("#categories .main_category").addClass("toggle_plus"); + $("#categories .main_category").removeClass("toggle_minus"); + } + // Put a minus image + if (!was_visible) + { + // Show the subcategories + $("#maincategory_" + id).toggle(); + $("#maincategory_" + id).parent().addClass("toggle_minus"); + $("#maincategory_" + id).parent().removeClass("toggle_plus"); + // Put a plus image + $("#maincategory_img_" + id).attr("src", category_minus); + settings.current_category = id; + } + if (!settings.category_accordion && was_visible) + { + $("#maincategory_" + id).toggle(); + $("#maincategory_" + id).parent().addClass("toggle_plus"); + $("#maincategory_" + id).parent().removeClass("toggle_minus"); + // Put a minus image + $("#maincategory_img_" + id).attr("src", category_plus); + } + }, + zoom: function (options) { + if ($.hasattr("category", options)) { + helpers.zoom_to_category(options["category"]); + } else if ($.hasattr("subcategories", options)) { + helpers.zoom_to_subcategories(options["subcategories"]); + } else if ($.hasattr("area", options)) { + helpers.zoom_to_area(options["area"]); + } } // end of init }; // End of public methods var helpers = { + getSubcategories: function (category_id) { + if(settings.get_subcategories_fx) { + return settings.get_subcategories_fx(category_id, settings); + } + else { + var ul = document.getElementById('maincategory_'+category_id); + var subcats = new Array(); + for (i in ul.children){ + var li = ul.children[i]; + if (li.id){ + subcats.push(li.id.split('_').pop()); + } + } + return subcats; + } + }, + retrieve_checked_categories: function () { + /* + * Retrieve checked_categories, and store it in settings + */ + var initialized = false; + $('#frm_categories .subcategories input:checkbox').each( + function(index){ + if (!initialized){ + initialized = true; + settings.checked_categories = []; + settings.display_submited = false; + } + if ($(this).attr('checked') == 'checked' || $(this).attr('checked') == true){ + cat_id = $(this).attr('id').split('_').pop(); + settings.checked_categories.push(parseInt(cat_id)); + } + }); + if(initialized && ($('#display_submited_check').attr("checked") == "checked" || $('#display_submited_check').attr("checked") == true)){ + settings.display_submited = true; + } + }, + zoom_to: function (bounds) { + settings.map.fitBounds(bounds); + }, + zoom_to_subcategories: function (ids) { + // TODO add markers and check the subcategory, if not yet checked/displayed + var ids = ids.join('_'); + if (!ids) ids = '0'; + var uri = extra_url + "getGeoObjects/" + ids; + if (settings.display_submited) uri += "/A_S"; + $.ajax({url: uri, + dataType: "json", + success: function (data) { + // Create a generic bounds + var lon, lat, feature; + var points = new Array(); + for (var i = 0; i < data.features.length; i++) { + feature = data.features[i]; + if (feature.geometry.type == 'Point') { + lat = feature.geometry.coordinates[1]; + lon = feature.geometry.coordinates[0]; + points.push(settings.map.latLngToLayerPoint([lat, lon])); + } else if (feature.geometry.type == 'LineString') { + // TODO + } + } + var bound = L.Bounds(points); + helpers.zoom_to(bounds); + } + }); + }, + zoom_to_category: function (id) { + helpers.zoom_to_subcategories(helpers.getSubcategories(id)); + }, + zoom_to_area: function (coords) { + /* zoom to an area */ + helpers.zoom_to([[coords[1], coords[0]], + [coords[3], coords[2]]]); + if (settings.dynamic_categories) { + methods.loadCategories(); + } + } }; // End of helpers $.fn.chimere = function (thing) { diff --git a/chimere/static/chimere/js/jquery.chimere-ol.js b/chimere/static/chimere/js/jquery.chimere-ol.js index 2e2d238..6660b2e 100644 --- a/chimere/static/chimere/js/jquery.chimere-ol.js +++ b/chimere/static/chimere/js/jquery.chimere-ol.js @@ -17,20 +17,22 @@ See the file COPYING for details. */ /* Add OpenLayers MapQuest layer management */ -OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { - name: "MapQuestOSM", - sphericalMercator: true, - url: ' http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png', - clone: function(obj) { - if (obj == null) { - obj = new OpenLayers.Layer.OSM( - this.name, this.url, this.getOptions()); - } - obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); - return obj; - }, - CLASS_NAME: "OpenLayers.Layer.MapQuestOSM" -}); +if (typeof(OpenLayers) != 'undefined'){ + OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { + name: "MapQuestOSM", + sphericalMercator: true, + url: ' http://otile1.mqcdn.com/tiles/1.0.0/osm/${z}/${x}/${y}.png', + clone: function(obj) { + if (obj == null) { + obj = new OpenLayers.Layer.OSM( + this.name, this.url, this.getOptions()); + } + obj = OpenLayers.Layer.XYZ.prototype.clone.apply(this, [obj]); + return obj; + }, + CLASS_NAME: "OpenLayers.Layer.MapQuestOSM" + }); +}; /* * Little hasattr helper @@ -82,16 +84,13 @@ OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { hide_popup_fx: null, // if leave to false every click on the map hide the pop-up explicit_popup_hide: false, - controls:[new OpenLayers.Control.Navigation(), - new OpenLayers.Control.PanPanel(), - new OpenLayers.Control.ZoomPanel(), - new OpenLayers.Control.ScaleLine()], - popupClass: OpenLayers.Popup.FramedCloud, + controls: null, + popupClass: null, popupContentFull: false, // if true the detail is inside the popup category_accordion: true, // category opening behave like an accordion maxResolution: 156543.0399, units: 'm', - projection: new OpenLayers.Projection('EPSG:4326'), + projection: null, theme: null, enable_clustering: false, routing: false, // enable routing management @@ -104,13 +103,10 @@ OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { current_category: null, // To store the current category clicked in list current_route_feature: null, // To store the current route find by routing itinerary_step_number:0, // current step number - icon_offset: new OpenLayers.Pixel(0, 0), + icon_offset: null, edition: false, // edition mode edition_type_is_route: false, // route or POI edition - default_icon: new OpenLayers.Icon( - 'http://www.openlayers.org/dev/img/marker-green.png', - new OpenLayers.Size(21, 25), - new OpenLayers.Pixel(-(21/2), -25)), + default_icon: null, cluster_icon: null, marker_hover_id:'marker_hover', marker_hover_content_id:'marker_hover_content', @@ -129,6 +125,27 @@ OpenLayers.Layer.MapQuestOSM = OpenLayers.Class(OpenLayers.Layer.XYZ, { */ init: function ( options ) { /* Manage parameters */ + if (defaults.controls == null){ + defaults.controls = [new OpenLayers.Control.Navigation(), + new OpenLayers.Control.PanPanel(), + new OpenLayers.Control.ZoomPanel(), + new OpenLayers.Control.ScaleLine()]; + } + if (defaults.popupClass == null){ + defaults.popupClass = OpenLayers.Popup.FramedCloud; + } + if (defaults.projection == null){ + defaults.projection = new OpenLayers.Projection('EPSG:4326'); + } + if (defaults.icon_offset == null){ + defaults.icon_offset = new OpenLayers.Pixel(0, 0); + } + if (defaults.default_icon == null){ + defaults.default_icon = new OpenLayers.Icon( + 'http://www.openlayers.org/dev/img/marker-green.png', + new OpenLayers.Size(21, 25), + new OpenLayers.Pixel(-(21/2), -25)); + } // not staticaly in default because of STATIC_URL init if (defaults.cluster_icon == null && typeof STATIC_URL != 'undefined'){ defaults.cluster_icon = new OpenLayers.Icon( diff --git a/chimere/templates/chimere/blocks/head_chimere.html b/chimere/templates/chimere/blocks/head_chimere.html index 099a044..dea92ec 100644 --- a/chimere/templates/chimere/blocks/head_chimere.html +++ b/chimere/templates/chimere/blocks/head_chimere.html @@ -1,5 +1,5 @@ -{% for css_url in MAP_CSS_URLS %} -{% endfor %} +{% for css_url, condition in MAP_CSS_URLS %} +{% if condition %}{%endif%}{% endfor %} {% for js_url in MAP_JS_URLS %} {% endfor %} {% if routing %}{% endif %} @@ -13,11 +13,12 @@ {% endfor %} diff --git a/chimere/templatetags/chimere_tags.py b/chimere/templatetags/chimere_tags.py index 90252a1..47f0800 100644 --- a/chimere/templatetags/chimere_tags.py +++ b/chimere/templatetags/chimere_tags.py @@ -144,7 +144,7 @@ def head_chimere(context, view=True): "DEFAULT_ZOOM": settings.CHIMERE_DEFAULT_ZOOM, "MAP_LAYER": settings.CHIMERE_DEFAULT_MAP_LAYER, "CHIMERE_VIEW_RENDERER": settings.CHIMERE_VIEW_RENDERER, - "MAP_CSS_URLS": settings.MAP_CSS_URLS[map_renderer], + "MAP_CONDITIONNAL_CSS_URLS": settings.MAP_CONDITIONNAL_CSS_URLS[map_renderer], "MAP_JS_URLS": settings.MAP_JS_URLS[map_renderer], 'routing': settings.CHIMERE_ENABLE_ROUTING \ if hasattr(settings, 'CHIMERE_ENABLE_ROUTING') else False -- cgit v1.2.3 From 1ab25dfc2603d6145a7ca6100558e470761ce766 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Fri, 5 Apr 2013 13:22:09 +0200 Subject: Weighted markers: model --- chimere/admin.py | 3 +- chimere/forms.py | 2 +- ...arker_weight__add_field_subcategory_weighted.py | 288 +++++++++++++++++++++ chimere/models.py | 8 + chimere/widgets.py | 2 +- 5 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 chimere/migrations/0004_auto__add_field_marker_weight__add_field_subcategory_weighted.py diff --git a/chimere/admin.py b/chimere/admin.py index c9107a5..c36fd26 100644 --- a/chimere/admin.py +++ b/chimere/admin.py @@ -192,7 +192,7 @@ class MarkerAdmin(admin.ModelAdmin): form = MarkerAdminForm fieldsets = ((None, { 'fields': ['point', 'name', 'status', 'categories', - 'description', 'start_date', 'end_date'] + 'description', 'weight', 'start_date', 'end_date'] }), (_(u"Submitter"), { 'classes':('collapse',), @@ -254,7 +254,6 @@ class MarkerAdmin(admin.ModelAdmin): ) return my_urls + urls - class RouteAdmin(MarkerAdmin): """ Specialized the Route field. diff --git a/chimere/forms.py b/chimere/forms.py index 655bbd3..ea23f94 100644 --- a/chimere/forms.py +++ b/chimere/forms.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2008-2012 Étienne Loks +# Copyright (C) 2008-2013 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as diff --git a/chimere/migrations/0004_auto__add_field_marker_weight__add_field_subcategory_weighted.py b/chimere/migrations/0004_auto__add_field_marker_weight__add_field_subcategory_weighted.py new file mode 100644 index 0000000..427bf86 --- /dev/null +++ b/chimere/migrations/0004_auto__add_field_marker_weight__add_field_subcategory_weighted.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Marker.weight' + db.add_column('chimere_marker', 'weight', + self.gf('django.db.models.fields.IntegerField')(default=0, null=True, blank=True), + keep_default=False) + + # Adding field 'SubCategory.weighted' + db.add_column('chimere_subcategory', 'weighted', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Marker.weight' + db.delete_column('chimere_marker', 'weight') + + # Deleting field 'SubCategory.weighted' + db.delete_column('chimere_subcategory', 'weighted') + + + models = { + 'chimere.aggregatedroute': { + 'Meta': {'object_name': 'AggregatedRoute', 'db_table': "'chimere_aggregated_routes'", 'managed': 'False'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'route': ('django.contrib.gis.db.models.fields.MultiLineStringField', [], {}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'subcategory': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.SubCategory']"}) + }, + 'chimere.area': { + 'Meta': {'ordering': "('order', 'name')", 'object_name': 'Area'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'default': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'default_subcategories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False', 'blank': 'True'}), + 'dynamic_categories': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'external_css': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layers': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'areas'", 'blank': 'True', 'through': "orm['chimere.AreaLayers']", 'to': "orm['chimere.Layer']"}), + 'lower_right_corner': ('django.contrib.gis.db.models.fields.PointField', [], {'default': "'POINT(0 0)'"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}), + 'restrict_to_extent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'subcategories': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'areas'", 'blank': 'True', 'db_table': "'chimere_subcategory_areas'", 'to': "orm['chimere.SubCategory']"}), + 'upper_left_corner': ('django.contrib.gis.db.models.fields.PointField', [], {'default': "'POINT(0 0)'"}), + 'urn': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'blank': 'True'}), + 'welcome_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.arealayers': { + 'Meta': {'ordering': "('order',)", 'object_name': 'AreaLayers'}, + 'area': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Area']"}), + 'default': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Layer']"}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.category': { + 'Meta': {'ordering': "['order']", 'object_name': 'Category'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.color': { + 'Meta': {'ordering': "['order']", 'object_name': 'Color'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '6'}), + 'color_theme': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.ColorTheme']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.colortheme': { + 'Meta': {'object_name': 'ColorTheme'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.icon': { + 'Meta': {'object_name': 'Icon'}, + 'height': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'width': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.importer': { + 'Meta': {'object_name': 'Importer'}, + 'associate_marker_to_way': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'default_description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'default_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'filtr': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'get_description': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'importer_type': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'source_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'srid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'zipped': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'chimere.layer': { + 'Meta': {'object_name': 'Layer'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_code': ('django.db.models.fields.TextField', [], {'max_length': '300'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.marker': { + 'Meta': {'ordering': "('status', 'name')", 'object_name': 'Marker'}, + 'available_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'import_key': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_version': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'is_front_page': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'modified_since_import': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'not_for_osm': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'point': ('chimere.widgets.PointField', [], {}), + 'ref_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'submited_marker'", 'null': 'True', 'to': "orm['chimere.Marker']"}), + 'route': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'associated_marker'", 'null': 'True', 'to': "orm['chimere.Route']"}), + 'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'submiter_comment': ('django.db.models.fields.TextField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'submiter_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'submiter_name': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'submiter_session_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.multimediaextension': { + 'Meta': {'object_name': 'MultimediaExtension'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'multimedia_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extensions'", 'to': "orm['chimere.MultimediaType']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '6'}) + }, + 'chimere.multimediafile': { + 'Meta': {'object_name': 'MultimediaFile'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'multimedia_files'", 'to': "orm['chimere.Marker']"}), + 'miniature': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'multimedia_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.MultimediaType']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'chimere.multimediatype': { + 'Meta': {'object_name': 'MultimediaType'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'iframe': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.news': { + 'Meta': {'object_name': 'News'}, + 'areas': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'to': "orm['chimere.Area']", 'null': 'True', 'blank': 'True'}), + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_front_page': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.page': { + 'Meta': {'object_name': 'Page'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mnemonic': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10', 'null': 'True', 'blank': 'True'}), + 'template_path': ('django.db.models.fields.CharField', [], {'max_length': '150', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.picturefile': { + 'Meta': {'object_name': 'PictureFile'}, + 'height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pictures'", 'to': "orm['chimere.Marker']"}), + 'miniature': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'picture': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'thumbnailfile': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'thumbnailfile_height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'thumbnailfile_width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.property': { + 'Meta': {'object_name': 'Property'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Marker']"}), + 'propertymodel': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.PropertyModel']"}), + 'value': ('django.db.models.fields.TextField', [], {}) + }, + 'chimere.propertymodel': { + 'Meta': {'ordering': "('order',)", 'object_name': 'PropertyModel'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mandatory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {}), + 'subcategories': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'properties'", 'blank': 'True', 'to': "orm['chimere.SubCategory']"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '1'}) + }, + 'chimere.propertymodelchoice': { + 'Meta': {'object_name': 'PropertyModelChoice'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'propertymodel': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'choices'", 'to': "orm['chimere.PropertyModel']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.route': { + 'Meta': {'ordering': "('status', 'name')", 'object_name': 'Route'}, + 'associated_file': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.RouteFile']", 'null': 'True', 'blank': 'True'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'has_associated_marker': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'import_key': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_version': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'modified_since_import': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'not_for_osm': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'ref_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'submited_route'", 'null': 'True', 'to': "orm['chimere.Route']"}), + 'route': ('chimere.widgets.RouteField', [], {}), + 'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'submiter_comment': ('django.db.models.fields.TextField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'submiter_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'submiter_name': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'submiter_session_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.routefile': { + 'Meta': {'ordering': "('name',)", 'object_name': 'RouteFile'}, + 'file_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'raw_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'simplified_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.subcategory': { + 'Meta': {'ordering': "['category', 'order']", 'object_name': 'SubCategory'}, + 'as_layer': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subcategories'", 'to': "orm['chimere.Category']"}), + 'color_theme': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.ColorTheme']", 'null': 'True', 'blank': 'True'}), + 'dated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'hover_icon': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'subcat_hovered'", 'null': 'True', 'to': "orm['chimere.Icon']"}), + 'icon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Icon']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1000'}), + 'routing_warn': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'submission': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'weighted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'chimere.tinyurl': { + 'Meta': {'object_name': 'TinyUrl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'parameters': ('django.db.models.fields.CharField', [], {'max_length': '500'}) + } + } + + complete_apps = ['chimere'] \ No newline at end of file diff --git a/chimere/models.py b/chimere/models.py index 545d7e0..3400118 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -213,6 +213,8 @@ class SubCategory(models.Model): available = models.BooleanField(_(u"Available"), default=True) submission = models.BooleanField(_(u"Available for submission"), default=True) + weighted = models.BooleanField(_(u"Has an associated quantity"), + default=False) TYPE = (('M', _(u'Marker')), ('R', _(u'Route')), ('B', _(u'Both')),) @@ -465,6 +467,8 @@ class Marker(GeographicItem): null=True) # used by feeds route = models.ForeignKey(u"Route", blank=True, null=True, related_name='associated_marker') + weight = models.IntegerField(_(u"Quantity"), blank=True, null=True, + default=0) description = models.TextField(_(u"Description"), blank=True, null=True) is_front_page = models.NullBooleanField(_(u"Is front page"), blank=True, null=True) @@ -534,6 +538,10 @@ class Marker(GeographicItem): def geom_attr(self): return 'point' + @property + def has_weight(self): + return bool(self.categories.filter(weighted=True).count()) + class Meta: ordering = ('status', 'name') verbose_name = _(u"Point of interest") diff --git a/chimere/widgets.py b/chimere/widgets.py index c2e8832..05752ce 100644 --- a/chimere/widgets.py +++ b/chimere/widgets.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (C) 2008-2012 Étienne Loks +# Copyright (C) 2008-2013 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as -- cgit v1.2.3 From 426eb4711ff92cc0e6df9cd1004ff322f112c814 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Fri, 5 Apr 2013 14:28:35 +0200 Subject: Weighted markers: first display with Leaflet --- chimere/models.py | 17 ++++++++++++++--- chimere/static/chimere/js/jquery.chimere-leaflet.js | 17 +++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/chimere/models.py b/chimere/models.py index 3400118..c269439 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -171,7 +171,8 @@ class Color(models.Model): """ code = models.CharField(_(u"Code"), max_length=6) order = models.IntegerField(_(u"Order")) - color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme")) + color_theme = models.ForeignKey(ColorTheme, verbose_name=_(u"Color theme"), + related_name='colors') def __unicode__(self): return self.code class Meta: @@ -637,14 +638,24 @@ class Marker(GeographicItem): if cat.hover_icon else '', 'icon_width':cat.icon.image.width, 'icon_height':cat.icon.image.height, - 'category_name':json.dumps(cat.name)} + 'category_name':json.dumps(cat.name),} + items['weight'] = '' + if cat.weighted: + if not self.weight: + continue + items['weight'] = u', "weight":%d' % self.weight + if cat.color_theme and cat.color_theme.colors.count(): + items['weight'] += u', "colors":["#%s"]' % '", "#'.join( + [color.code for color in cat.color_theme.colors.\ + order_by('order').all()]) jsons.append(u'{"type":"Feature", "geometry":%(geometry)s, '\ u'"properties":{"pk": %(id)d, "name": %(name)s, '\ u'"icon_path":"%(icon_path)s", '\ u'"icon_hover_path":"%(icon_hover_path)s", '\ u'"icon_width":%(icon_width)d, '\ u'"icon_height":%(icon_height)d, '\ - u'"category_name":%(category_name)s}}' % items) + u'"category_name":%(category_name)s'\ + u'%(weight)s}}' % items) return ",".join(jsons) @property diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 569f9f5..5e0e811 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -125,12 +125,21 @@ See the file COPYING for details. layer.bindPopup(feature.properties.name); }, pointToLayer: function (feature, latlng) { - if (feature.properties.weigthed){ + if (feature.properties.weight){ + var fill_color = "#ff7800"; + if (feature.properties.colors){ + var idx = feature.properties.weight/5; + if (idx < feature.properties.colors.length){ + fill_color = feature.properties.colors[idx]; + } else { + fill_color = feature.properties.colors[feature.properties.colors.length-1]; + } + } var geojsonMarkerOptions = { - radius: 8, - fillColor: "#ff7800", + radius: feature.properties.weight*2, + fillColor: fill_color, color: "#000", - weight: 1, + weight: feature.properties.weight/2, opacity: 1, fillOpacity: 0.8 }; -- cgit v1.2.3 From 2e9154a16b03c586450e15ec931996db3254f9fd Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Fri, 5 Apr 2013 18:06:06 +0200 Subject: Leaflet: work on clustering --- chimere/settings.sample.py | 19 ++- .../static/chimere/js/jquery.chimere-leaflet.js | 151 +++++++++++++-------- chimere/static/leaflet-markercluster/CHANGELOG.md | 49 +++++++ .../static/leaflet-markercluster/MIT-LICENCE.txt | 20 +++ .../MarkerCluster.Default.css | 38 ++++++ .../MarkerCluster.Default.ie.css | 22 +++ .../static/leaflet-markercluster/MarkerCluster.css | 6 + chimere/static/leaflet-markercluster/README.md | 125 +++++++++++++++++ .../leaflet-markercluster/leaflet.markercluster.js | 6 + chimere/templates/chimere/blocks/map.html | 1 + chimere/templatetags/chimere_tags.py | 4 + 11 files changed, 381 insertions(+), 60 deletions(-) create mode 100644 chimere/static/leaflet-markercluster/CHANGELOG.md create mode 100644 chimere/static/leaflet-markercluster/MIT-LICENCE.txt create mode 100644 chimere/static/leaflet-markercluster/MarkerCluster.Default.css create mode 100644 chimere/static/leaflet-markercluster/MarkerCluster.Default.ie.css create mode 100644 chimere/static/leaflet-markercluster/MarkerCluster.css create mode 100644 chimere/static/leaflet-markercluster/README.md create mode 100644 chimere/static/leaflet-markercluster/leaflet.markercluster.js diff --git a/chimere/settings.sample.py b/chimere/settings.sample.py index da703fb..b23f31e 100644 --- a/chimere/settings.sample.py +++ b/chimere/settings.sample.py @@ -76,6 +76,8 @@ CHIMERE_SHAPEFILE_ENCODING = 'ISO-8859-1' # it could be a good idea to hide it to an admin who could'nt do that CHIMERE_HIDE_PROPERTYMODEL = False +CHIMERE_ENABLE_CLUSTERING = False + # enable routing in Chimère CHIMERE_ENABLE_ROUTING = False @@ -297,13 +299,21 @@ if 'MAP_JS_URLS' not in globals(): STATIC_URL + "chimere/js/jquery.chimere-leaflet.js" ] } - + if CHIMERE_ENABLE_CLUSTERING: + MAP_JS_URLS['leaflet'] += [ + STATIC_URL + "leaflet-markercluster/leaflet.markercluster.js" + ] if 'MAP_CSS_URLS' not in globals(): global MAP_CSS_URLS MAP_CSS_URLS = { 'openlayers':["http://www.openlayers.org/api/theme/default/style.css"], 'leaflet':[STATIC_URL + "leaflet/leaflet.css"], } + if CHIMERE_ENABLE_CLUSTERING: + MAP_CSS_URLS['leaflet'] += [ + STATIC_URL + "leaflet-markercluster/MarkerCluster.css", + STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.css", + ] # key: [(url, condition)] MAP_CONDITIONNAL_CSS_URLS = { 'openlayers':[("http://www.openlayers.org/api/theme/default/style.css", @@ -313,4 +323,11 @@ if 'MAP_CSS_URLS' not in globals(): (STATIC_URL + "leaflet/leaflet.ie.css", 'lte IE 8') ] } + if CHIMERE_ENABLE_CLUSTERING: + MAP_CSS_URLS['leaflet'] += [ + (STATIC_URL + "leaflet-markercluster/MarkerCluster.css", None), + (STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.css", None), + (STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.ie.css", + 'lte IE 8'), + ] diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 5e0e811..36729c1 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -120,58 +120,77 @@ See the file COPYING for details. map.fitWorld(); } settings.icons = new Object(); - settings.layerMarkers = L.geoJson(null, { - onEachFeature: function (feature, layer) { - layer.bindPopup(feature.properties.name); - }, - pointToLayer: function (feature, latlng) { - if (feature.properties.weight){ - var fill_color = "#ff7800"; - if (feature.properties.colors){ - var idx = feature.properties.weight/5; - if (idx < feature.properties.colors.length){ - fill_color = feature.properties.colors[idx]; - } else { - fill_color = feature.properties.colors[feature.properties.colors.length-1]; - } + if (settings.enable_clustering){ + settings.layerMarkers = new L.MarkerClusterGroup({ + iconCreateFunction: function (cluster) { + var markers = cluster.getAllChildMarkers(); + var weight = 0; + for (idx=0;idx' + cluster.getChildCount() + '' }); + } +}); +``` +Check out the [custom example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-custom.html) for an example of this. + +### All Options +Enabled by default (boolean options): +* **zoomToBoundsOnClick**: When you click a cluster we zoom to its bounds. +* **showCoverageOnHover**: When you mouse over a cluster it shows the bounds of its markers. +* **spiderfyOnMaxZoom**: When you click a cluster at the bottom zoom level we spiderfy it so you can see all of its markers. + +Other options +* **animateAddingMarkers**: If set to true then adding individual markers to the MarkerClusterGroup after it has been added to the map will add the marker and animate it in to the cluster. Defaults to false as this gives better performance when bulk adding markers. +* **disableClusteringAtZoom**: If set, at this zoom level and below markers will not be clustered. This defaults to disabled. [See Example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld-maxzoom.388.html) +* **maxClusterRadius**: The maximum radius that a cluster will cover from the central marker (in pixels). Default 80. Decreasing will make more smaller clusters. +* **polygonOptions**: Options to pass when creating the L.Polygon to show the bounds of a cluster +* **singleMarkerMode**: If set to true, overrides the icon for all added markers to make them appear as a 1 size cluster +* **spiderfyDistanceMultiplier**: Increase from 1 to increase the distance away from the center that spiderfied markers are placed. Use if you are using big marker icons. + +## Events +If you register for click, mouseover, etc events just related to Markers in the cluster. +To recieve events for clusters listen to 'cluster' + 'eventIWant', ex: 'clusterclick', 'clustermouseover'. + +Set your callback up as follows to handle both cases: + +```javascript +markers.on('click', function (a) { + console.log('marker ' + a.layer); +}); + +markers.on('clusterclick', function (a) { + console.log('cluster ' + a.layer.getAllChildMarkers().length); +}); +``` + +## Methods + +### Getting the bounds of a cluster +When you recieve an event from a cluster you can query it for the bounds. +See [example/marker-clustering-convexhull.html](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-convexhull.html) for a working example. +```javascript +markers.on('clusterclick', function (a) { + map.addLayer(new L.Polygon(a.layer.getConvexHull())); +}); +``` + +### Zooming to the bounds of a cluster +When you recieve an event from a cluster you can zoom to its bounds in one easy step. +See [marker-clustering-zoomtobounds.html](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-zoomtobounds.html) for a working example. +```javascript +markers.on('clusterclick', function (a) { + a.layer.zoomToBounds(); +}); +``` + +### Adding and removing Markers +addLayer, removeLayer and clearLayers are supported and they should work for most uses. + +### Bulk adding and removing Markers +addLayers and removeLayers are bulk methods for adding and removing markers and should be favoured over the single versions when doing bulk addition/removal of markers. Each takes an array of markers + +If you are removing a lot of markers it will almost definitely be better to call clearLayers then call addLayers to add the markers you don't want to remove back in. See [#59](https://github.com/Leaflet/Leaflet.markercluster/issues/59#issuecomment-9320628) for details. + +### Other Methods +```` +hasLayer(layer): Returns true if the given layer (marker) is in the MarkerClusterGroup +zoomToShowLayer(layer, callback): Zooms to show the given marker (spidifying if required), calls the callback when the marker is visible on the map +addLayers(layerArray): Adds the markers in the given array from the MarkerClusterGroup in an efficent bulk method. +removeLayers(layerArray): Removes the markers in the given array from the MarkerClusterGroup in an efficent bulk method. +```` + +## Handling LOTS of markers +The Clusterer can handle 10000 or even 50000 markers (in chrome). IE9 has some issues with 50000. +[realworld 10000 example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld.10000.html) +[realworld 50000 example](http://leaflet.github.com/Leaflet.markercluster/example/marker-clustering-realworld.50000.html) +Performance optimizations could be done so these are handled more gracefully (Running the initial clustering over multiple JS calls rather than locking the browser for a long time) + +### License + +Leaflet.markercluster is free software, and may be redistributed under the MIT-LICENSE. diff --git a/chimere/static/leaflet-markercluster/leaflet.markercluster.js b/chimere/static/leaflet-markercluster/leaflet.markercluster.js new file mode 100644 index 0000000..74c25c3 --- /dev/null +++ b/chimere/static/leaflet-markercluster/leaflet.markercluster.js @@ -0,0 +1,6 @@ +/* + Copyright (c) 2012, Smartrak, David Leaver + Leaflet.markercluster is an open-source JavaScript library for Marker Clustering on leaflet powered maps. + https://github.com/danzel/Leaflet.markercluster +*/ +(function(e,t){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),L.FeatureGroup.prototype.initialize.call(this,[]),this._inZoomAnimation=0,this._needsClustering=[],this._currentShownBounds=null},addLayer:function(e){if(e instanceof L.LayerGroup){var t=[];for(var n in e._layers)e._layers.hasOwnProperty(n)&&t.push(e._layers[n]);return this.addLayers(t)}if(!this._map)return this._needsClustering.push(e),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom);var r=e,i=this._map.getZoom();if(e.__parent)while(r.__parent._zoom>=i)r=r.__parent;return this._currentShownBounds.contains(r.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,r):this._animationAddLayerNonAnimated(e,r)),this},removeLayer:function(e){return this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),e._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1)),this):this:(this._arraySplice(this._needsClustering,e),this)},addLayers:function(e){var t,n,r;if(!this._map)return this._needsClustering=this._needsClustering.concat(e),this;for(t=0,n=e.length;t=0;t--)e.extend(this._needsClustering[t].getLatLng());return e},eachLayer:function(e,t){var n=this._needsClustering.slice(),r;this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n);for(r=n.length-1;r>=0;r--)e.call(t,n[r])},hasLayer:function(e){if(this._needsClustering.length>0){var t=this._needsClustering;for(var n=t.length-1;n>=0;n--)if(t[n]===e)return!0}return!!e.__parent&&e.__parent._group===this},zoomToShowLayer:function(e,t){var n=function(){if((e._icon||e.__parent._icon)&&!this._inZoomAnimation){this._map.off("moveend",n,this),this.off("animationend",n,this);if(e._icon)t();else if(e.__parent._icon){var r=function(){this.off("spiderfied",r,this),t()};this.on("spiderfied",r,this),e.__parent.spiderfy()}}};e._icon?t():e.__parent._zoom=0;n--)if(e[n]===t){e.splice(n,1);return}},_removeLayer:function(e,t,n){var r=this._gridClusters,i=this._gridUnclustered,s=this._map;if(t)for(var o=this._maxZoom;o>=0;o--)if(!i[o].removeObject(e,s.project(e.getLatLng(),o)))break;var u=e.__parent,a=u._markers,f;this._arraySplice(a,e);while(u){u._childCount--;if(u._zoom<0)break;t&&u._childCount<=1?(f=u._markers[0]===e?u._markers[1]:u._markers[0],r[u._zoom].removeObject(u,s.project(u._cLatLng,u._zoom)),i[u._zoom].addObject(f,s.project(f.getLatLng(),u._zoom)),this._arraySplice(u.__parent._childClusters,u),u.__parent._markers.push(f),f.__parent=u.__parent,u._icon&&(L.FeatureGroup.prototype.removeLayer.call(this,u),n||L.FeatureGroup.prototype.addLayer.call(this,f))):(u._recalculateBounds(),(!n||!u._icon)&&u._updateIcon()),u=u.__parent}delete e.__parent},_propagateEvent:function(e){e.target instanceof L.MarkerCluster&&(e.type="cluster"+e.type),L.FeatureGroup.prototype._propagateEvent.call(this,e)},_defaultIconCreateFunction:function(e){var t=e.getChildCount(),n=" marker-cluster-";return t<10?n+="small":t<100?n+="medium":n+="large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+n,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=null,t=this._map,n=this.options.spiderfyOnMaxZoom,r=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick;(n||i)&&this.on("clusterclick",function(e){t.getMaxZoom()===t.getZoom()?n&&e.layer.spiderfy():i&&e.layer.zoomToBounds()},this),r&&(this.on("clustermouseover",function(n){if(this._inZoomAnimation)return;e&&t.removeLayer(e),n.layer.getChildCount()>2&&n.layer!==this._spiderfied&&(e=new L.Polygon(n.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(e))},this),this.on("clustermouseout",function(){e&&(t.removeLayer(e),e=null)},this),t.on("zoomend",function(){e&&(t.removeLayer(e),e=null)},this),t.on("layerremove",function(n){e&&n.layer===this&&(t.removeLayer(e),e=null)},this))},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick,r=this._map;(e||n)&&this.off("clusterclick",null,this),t&&(this.off("clustermouseover",null,this),this.off("clustermouseout",null,this),r.off("zoomend",null,this),r.off("layerremove",null,this))},_zoomEnd:function(){if(!this._map)return;this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds()},_moveEnd:function(){if(this._inZoomAnimation)return;var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,e),this._currentShownBounds=e;return},_generateInitialClusters:function(){var e=this._map.getMaxZoom(),t=this.options.maxClusterRadius;this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var n=e;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(t),this._gridUnclustered[n]=new L.DistanceGrid(t);this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(e,t){var n=this._gridClusters,r=this._gridUnclustered,i,s;this.options.singleMarkerMode&&(e.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[e]}}));for(;t>=0;t--){i=this._map.project(e.getLatLng(),t);var o=n[t].getNearObject(i);if(o){o._addChild(e),e.__parent=o;return}o=r[t].getNearObject(i);if(o){var u=o.__parent;u&&this._removeLayer(o,!1);var a=new L.MarkerCluster(this,t,o,e);n[t].addObject(a,this._map.project(a._cLatLng,t)),o.__parent=a,e.__parent=a;var f=a;for(s=t-1;s>u._zoom;s--)f=new L.MarkerCluster(this,s,f),n[s].addObject(f,this._map.project(o.getLatLng(),s));u._addChild(f);for(s=t;s>=0;s--)if(!r[s].removeObject(o,this._map.project(o.getLatLng(),s)))break;return}r[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel;return},_mergeSplitClusters:function(){this._zoomthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this.getBounds();var e=this._map,t=e.getBounds(),n=t._southWest,r=t._northEast,i=L.Browser.mobile?0:Math.abs(n.lat-r.lat),s=L.Browser.mobile?0:Math.abs(n.lng-r.lng);return new L.LatLngBounds(new L.LatLng(n.lat-i,n.lng-s,!0),new L.LatLng(r.lat+i,r.lng+s,!0))},_animationAddLayerNonAnimated:function(e,t){if(t===e)L.FeatureGroup.prototype.addLayer.call(this,e);else if(t._childCount===2){t._addToMap();var n=t.getAllChildMarkers();L.FeatureGroup.prototype.removeLayer.call(this,n[0]),L.FeatureGroup.prototype.removeLayer.call(this,n[1])}else t._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(e,t){var n=this,r=this._getExpandedVisibleBounds(),i;this._topClusterLevel._recursively(r,e,0,function(s){var o=s._latlng,u=s._markers,a;s._isSingleParent()&&e+1===t?(L.FeatureGroup.prototype.removeLayer.call(n,s),s._recursivelyAddChildrenToMap(null,t,r)):(s.setOpacity(0),s._recursivelyAddChildrenToMap(o,t,r));for(i=u.length-1;i>=0;i--)a=u[i],r.contains(a._latlng)||L.FeatureGroup.prototype.removeLayer.call(n,a)}),this._forceLayout();var s,o;n._topClusterLevel._recursivelyBecomeVisible(r,t);for(s in n._layers)n._layers.hasOwnProperty(s)&&(o=n._layers[s],!(o instanceof L.MarkerCluster)&&o._icon&&o.setOpacity(1));n._topClusterLevel._recursively(r,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),setTimeout(function(){n._topClusterLevel._recursively(r,e,0,function(e){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1)}),n._animationEnd()},200)},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(e,t,n){var r=this._getExpandedVisibleBounds();e._recursivelyAnimateChildrenInAndAddSelfToMap(r,t+1,n);var i=this;this._forceLayout(),e._recursivelyBecomeVisible(r,n),setTimeout(function(){if(e._childCount===1){var s=e._markers[0];s.setLatLng(s.getLatLng()),s.setOpacity(1);return}e._recursively(r,n,0,function(e){e._recursivelyRemoveChildrenFromMap(r,t+1)}),i._animationEnd()},200)},_animationAddLayer:function(e,t){var n=this;L.FeatureGroup.prototype.addLayer.call(this,e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.setOpacity(0),setTimeout(function(){L.FeatureGroup.prototype.removeLayer.call(n,e),e.setOpacity(1),n._animationEnd()},200)):(this._forceLayout(),n._animationStart(),n._animationZoomOutSingle(t,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationZoomOut:function(e,t){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){this._animationAddLayerNonAnimated(e,t)}}),L.MarkerCluster=L.Marker.extend({initialize:function(e,t,n,r){L.Marker.prototype.initialize.call(this,n?n._cLatLng||n.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,n&&this._addChild(n),r&&this._addChild(r)},getAllChildMarkers:function(e){e=e||[];for(var t=this._childClusters.length-1;t>=0;t--)this._childClusters[t].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(){this._group._map.fitBounds(this._bounds)},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._expandBounds(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_expandBounds:function(e){var t,n=e._wLatLng||e._latlng;e instanceof L.MarkerCluster?(this._bounds.extend(e._bounds),t=e._childCount):(this._bounds.extend(n),t=1),this._cLatLng||(this._cLatLng=e._cLatLng||n);var r=this._childCount+t;this._wLatLng?(this._wLatLng.lat=(n.lat*t+this._wLatLng.lat*this._childCount)/r,this._wLatLng.lng=(n.lng*t+this._wLatLng.lng*this._childCount)/r):this._latlng=this._wLatLng=new L.LatLng(n.lat,n.lng)},_addToMap:function(e){e&&(this._backupLatlng=this._latlng,this.setLatLng(e)),L.FeatureGroup.prototype.addLayer.call(this._group,this)},_recursivelyAnimateChildrenIn:function(e,t,n){this._recursively(e,0,n-1,function(e){var n=e._markers,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))},function(e){var n=e._childClusters,r,i;for(r=n.length-1;r>=0;r--)i=n[r],i._icon&&(i._setPos(t),i.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,n){this._recursively(e,n,0,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),t),r._isSingleParent()&&t-1===n?(r.setOpacity(1),r._recursivelyRemoveChildrenFromMap(e,t)):r.setOpacity(0),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,0,t,null,function(e){e.setOpacity(1)})},_recursivelyAddChildrenToMap:function(e,t,n){this._recursively(n,-1,t,function(r){if(t===r._zoom)return;for(var i=r._markers.length-1;i>=0;i--){var s=r._markers[i];if(!n.contains(s._latlng))continue;e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.setOpacity(0)),L.FeatureGroup.prototype.addLayer.call(r._group,s)}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var n=this._markers[t];n._backupLatlng&&(n.setLatLng(n._backupLatlng),delete n._backupLatlng)}if(e-1===this._zoom)for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._restorePosition();else for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,n){var r,i;this._recursively(e,-1,t-1,function(e){for(i=e._markers.length-1;i>=0;i--){r=e._markers[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}},function(e){for(i=e._childClusters.length-1;i>=0;i--){r=e._childClusters[i];if(!n||!n.contains(r._latlng))L.FeatureGroup.prototype.removeLayer.call(e._group,r),r.setOpacity(1)}})},_recursively:function(e,t,n,r,i){var s=this._childClusters,o=this._zoom,u,a;if(t>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i);else{r&&r(this),i&&this._zoom===n&&i(this);if(n>o)for(u=s.length-1;u>=0;u--)a=s[u],e.intersects(a._bounds)&&a._recursively(e,t,n,r,i)}},_recalculateBounds:function(){var e=this._markers,t=this._childClusters,n;this._bounds=new L.LatLngBounds,delete this._wLatLng;for(n=e.length-1;n>=0;n--)this._expandBounds(e[n]);for(n=t.length-1;n>=0;n--)this._expandBounds(t[n])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u=L.Util.stamp(e);this._objectPoint[u]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var n=this._getCoord(t.x),r=this._getCoord(t.y),i=this._grid,s=i[r]=i[r]||{},o=s[n]=s[n]||[],u,a;delete this._objectPoint[L.Util.stamp(e)];for(u=0,a=o.length;u=0;s--){o=t[s],u=this.getDistant(o,e);if(!(u>0))continue;i.push(o),u>n&&(n=u,r=o)}return{maxPoint:r,newPoints:i}},buildConvexHull:function(e,t){var n=[],r=this.findMostDistantPointFromBaseLine(e,t);return r.maxPoint?(n=n.concat(this.buildConvexHull([e[0],r.maxPoint],r.newPoints)),n=n.concat(this.buildConvexHull([r.maxPoint,e[1]],r.newPoints)),n):[e]},getConvexHull:function(e){var t=!1,n=!1,r=null,i=null,s;for(s=e.length-1;s>=0;s--){var o=e[s];if(t===!1||o.lat>t)r=o,t=o.lat;if(n===!1||o.lat=0;s--)i=e[s].getLatLng(),t.push(i);r=L.QuickHull.getConvexHull(t);for(s=r.length-1;s>=0;s--)n.push(r[s][0]);return n}}),L.MarkerCluster.include({_2PI:Math.PI*2,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied===this||this._group._inZoomAnimation)return;var e=this.getAllChildMarkers(),t=this._group,n=t._map,r=n.latLngToLayerPoint(this._latlng),i;this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?i=this._generatePointsSpiral(e.length,r):(r.y+=10,i=this._generatePointsCircle(e.length,r)),this._animationSpiderfy(e,i)},unspiderfy:function(e){if(this._group._inZoomAnimation)return;this._animationUnspiderfy(e),this._group._spiderfied=null},_generatePointsCircle:function(e,t){var n=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),r=n/this._2PI,i=this._2PI/e,s=[],o,u;s.length=e;for(o=e-1;o>=0;o--)u=this._circleStartAngle+o*i,s[o]=(new L.Point(t.x+r*Math.cos(u),t.y+r*Math.sin(u)))._round();return s},_generatePointsSpiral:function(e,t){var n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,r=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,i=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,s=0,o=[],u;o.length=e;for(u=e-1;u>=0;u--)s+=r/n+u*5e-4,o[u]=(new L.Point(t.x+n*Math.cos(s),t.y+n*Math.sin(s)))._round(),n+=this._2PI*i/s;return o}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return(document.createElementNS("http://www.w3.org/2000/svg","animate")+"").indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(e,t){var n=this,r=this._group,i=r._map,s=i.latLngToLayerPoint(this._latlng),o,u,a,f;for(o=e.length-1;o>=0;o--)u=e[o],u.setZIndexOffset(1e6),u.setOpacity(0),L.FeatureGroup.prototype.addLayer.call(r,u),u._setPos(s);r._forceLayout(),r._animationStart();var l=L.Path.SVG?0:.3,c=L.Path.SVG_NS;for(o=e.length-1;o>=0;o--){f=i.layerPointToLatLng(t[o]),u=e[o],u._preSpiderfyLatlng=u._latlng,u.setLatLng(f),u.setOpacity(1),a=new L.Polyline([n._latlng,f],{weight:1.5,color:"#222",opacity:l}),i.addLayer(a),u._spiderLeg=a;if(!L.Path.SVG||!this.SVG_ANIMATION)continue;var h=a._path.getTotalLength();a._path.setAttribute("stroke-dasharray",h+","+h);var p=document.createElementNS(c,"animate");p.setAttribute("attributeName","stroke-dashoffset"),p.setAttribute("begin","indefinite"),p.setAttribute("from",h),p.setAttribute("to",0),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement(),p=document.createElementNS(c,"animate"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("attributeName","stroke-opacity"),p.setAttribute("begin","indefinite"),p.setAttribute("from",0),p.setAttribute("to",.5),p.setAttribute("dur",.25),a._path.appendChild(p),p.beginElement()}n.setOpacity(.3);if(L.Path.SVG){this._group._forceLayout();for(o=e.length-1;o>=0;o--)u=e[o]._spiderLeg,u.options.opacity=.5,u._path.setAttribute("stroke-opacity",.5)}setTimeout(function(){r._animationEnd(),r.fire("spiderfied")},200)},_animationUnspiderfy:function(e){var t=this._group,n=t._map,r=e?n._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):n.latLngToLayerPoint(this._latlng),i=this.getAllChildMarkers(),s=L.Path.SVG&&this.SVG_ANIMATION,o,u,a;t._animationStart(),this.setOpacity(1);for(u=i.length-1;u>=0;u--){o=i[u];if(!o._preSpiderfyLatlng)continue;o.setLatLng(o._preSpiderfyLatlng),delete o._preSpiderfyLatlng,o._setPos(r),o.setOpacity(0),s&&(a=o._spiderLeg._path.childNodes[0],a.setAttribute("to",a.getAttribute("from")),a.setAttribute("from",0),a.beginElement(),a=o._spiderLeg._path.childNodes[1],a.setAttribute("from",.5),a.setAttribute("to",0),a.setAttribute("stroke-opacity",0),a.beginElement(),o._spiderLeg._path.setAttribute("stroke-opacity",0))}setTimeout(function(){var e=0;for(u=i.length-1;u>=0;u--)o=i[u],o._spiderLeg&&e++;for(u=i.length-1;u>=0;u--){o=i[u];if(!o._spiderLeg)continue;o.setOpacity(1),o.setZIndexOffset(0),e>1&&L.FeatureGroup.prototype.removeLayer.call(t,o),n.removeLayer(o._spiderLeg),delete o._spiderLeg}t._animationEnd()},200)}}:{_animationSpiderfy:function(e,t){var n=this._group,r=n._map,i,s,o,u;for(i=e.length-1;i>=0;i--)u=r.layerPointToLatLng(t[i]),s=e[i],s._preSpiderfyLatlng=s._latlng,s.setLatLng(u),s.setZIndexOffset(1e6),L.FeatureGroup.prototype.addLayer.call(n,s),o=new L.Polyline([this._latlng,u],{weight:1.5,color:"#222"}),r.addLayer(o),s._spiderLeg=o;this.setOpacity(.3),n.fire("spiderfied")},_animationUnspiderfy:function(){var e=this._group,t=e._map,n=this.getAllChildMarkers(),r,i;this.setOpacity(1);for(i=n.length-1;i>=0;i--)r=n[i],L.FeatureGroup.prototype.removeLayer.call(e,r),r.setLatLng(r._preSpiderfyLatlng),delete r._preSpiderfyLatlng,r.setZIndexOffset(0),t.removeLayer(r._spiderLeg),delete r._spiderLeg}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation?this._map.on("zoomstart",this._unspiderfyZoomStart,this):this._map.on("zoomend",this._unspiderfyWrapper,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){if(!this._map)return;this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){if(L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching"))return;this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e)},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_unspiderfyLayer:function(e){e._spiderLeg&&(L.FeatureGroup.prototype.removeLayer.call(this,e),e.setOpacity(1),e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}})})(this); \ No newline at end of file diff --git a/chimere/templates/chimere/blocks/map.html b/chimere/templates/chimere/blocks/map.html index 6d4508d..2614f39 100644 --- a/chimere/templates/chimere/blocks/map.html +++ b/chimere/templates/chimere/blocks/map.html @@ -50,6 +50,7 @@ {% if restricted_extent %}{{ restricted_extent }} chimere_init_options["restricted_extent"] = bounds; {% endif %} + {% if enable_clustering %}chimere_init_options['enable_clustering'] = true;{% endif%} {% if selected_map_layer %} chimere_init_options["selected_map_layer"] = {{selected_map_layer}}; {% endif %} diff --git a/chimere/templatetags/chimere_tags.py b/chimere/templatetags/chimere_tags.py index 47f0800..f180061 100644 --- a/chimere/templatetags/chimere_tags.py +++ b/chimere/templatetags/chimere_tags.py @@ -190,6 +190,10 @@ def map(context, map_id='map'): context_data['icon_offset_y'] = settings.CHIMERE_ICON_OFFSET_Y context_data['icon_width'] = settings.CHIMERE_ICON_WIDTH context_data['icon_height'] = settings.CHIMERE_ICON_HEIGHT + context_data['enable_clustering'] = True \ + if hasattr(settings, 'CHIMERE_ENABLE_CLUSTERING') and \ + settings.CHIMERE_ENABLE_CLUSTERING \ + else False context_data['routing'] = 'true' \ if hasattr(settings, 'CHIMERE_ENABLE_ROUTING') and \ settings.CHIMERE_ENABLE_ROUTING \ -- cgit v1.2.3 From 54facb137f45e9ea5fc774203c45d508184973d5 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Sun, 7 Apr 2013 21:49:04 +0200 Subject: Leaflet: first fully functionnal version of clustering --- chimere/settings.sample.py | 20 ++---- chimere/static/chimere/css/styles.css | 51 ++++++++++++++ .../static/chimere/img/marker-cluster-large.png | Bin 0 -> 1182 bytes .../static/chimere/img/marker-cluster-medium.png | Bin 0 -> 861 bytes .../static/chimere/img/marker-cluster-small.png | Bin 0 -> 701 bytes .../static/chimere/js/jquery.chimere-leaflet.js | 75 ++++++++++++++++++--- 6 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 chimere/static/chimere/img/marker-cluster-large.png create mode 100644 chimere/static/chimere/img/marker-cluster-medium.png create mode 100644 chimere/static/chimere/img/marker-cluster-small.png diff --git a/chimere/settings.sample.py b/chimere/settings.sample.py index b23f31e..22502b2 100644 --- a/chimere/settings.sample.py +++ b/chimere/settings.sample.py @@ -307,27 +307,21 @@ if 'MAP_CSS_URLS' not in globals(): global MAP_CSS_URLS MAP_CSS_URLS = { 'openlayers':["http://www.openlayers.org/api/theme/default/style.css"], - 'leaflet':[STATIC_URL + "leaflet/leaflet.css"], + 'leaflet':[STATIC_URL + "leaflet/leaflet.css", + STATIC_URL + "leaflet-markercluster/MarkerCluster.css", + STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.css", + ] } - if CHIMERE_ENABLE_CLUSTERING: - MAP_CSS_URLS['leaflet'] += [ - STATIC_URL + "leaflet-markercluster/MarkerCluster.css", - STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.css", - ] # key: [(url, condition)] MAP_CONDITIONNAL_CSS_URLS = { 'openlayers':[("http://www.openlayers.org/api/theme/default/style.css", None)], 'leaflet':[ - (STATIC_URL + "leaflet/leaflet.css", None), - (STATIC_URL + "leaflet/leaflet.ie.css", 'lte IE 8') - ] - } - if CHIMERE_ENABLE_CLUSTERING: - MAP_CSS_URLS['leaflet'] += [ + (STATIC_URL + "leaflet/leaflet.css", None), + (STATIC_URL + "leaflet/leaflet.ie.css", 'lte IE 8') (STATIC_URL + "leaflet-markercluster/MarkerCluster.css", None), (STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.css", None), (STATIC_URL + "leaflet-markercluster/MarkerCluster.Default.ie.css", 'lte IE 8'), ] - + } diff --git a/chimere/static/chimere/css/styles.css b/chimere/static/chimere/css/styles.css index 764e0c7..a123a5d 100644 --- a/chimere/static/chimere/css/styles.css +++ b/chimere/static/chimere/css/styles.css @@ -1109,3 +1109,54 @@ div.pp_default .pp_expand{ left:6px; bottom:5px; } + +.marker-cluster.marker-cluster-large { + border-radius: 25px; +} + +.marker-cluster.marker-cluster-small { + border-radius: 15px; +} + +.marker-cluster.marker-cluster-large div { + width: 40px; + height: 40px; + border-radius: 20px; +} + +.marker-cluster.marker-cluster-small div { + width: 20px; + height: 20px; + border-radius: 10px; +} + +.marker-cluster.marker-cluster-large span { + line-height: 40px; +} + +.marker-cluster.marker-cluster-small span { + line-height: 20px; +} + +.marker-cluster-small-number, +.marker-cluster-medium-number, +.marker-cluster-large-number{ + position: relative; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + text-align: center; +} + +.marker-cluster-small-number{ + width: 30px; + top: -22px; +} + +.marker-cluster-medium-number{ + width: 40px; + top: -27px; +} + +.marker-cluster-large-number{ + width: 50px; + top: -32px; +} diff --git a/chimere/static/chimere/img/marker-cluster-large.png b/chimere/static/chimere/img/marker-cluster-large.png new file mode 100644 index 0000000..477e2b8 Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-large.png differ diff --git a/chimere/static/chimere/img/marker-cluster-medium.png b/chimere/static/chimere/img/marker-cluster-medium.png new file mode 100644 index 0000000..1d9099b Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-medium.png differ diff --git a/chimere/static/chimere/img/marker-cluster-small.png b/chimere/static/chimere/img/marker-cluster-small.png new file mode 100644 index 0000000..ae5ce5a Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-small.png differ diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 36729c1..ef156fb 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -95,7 +95,12 @@ See the file COPYING for details. marker_hover_offset: null, icon_start: null, icon_step: null, - icon_end: null + icon_end: null, + weight_steps: new Array(6, 15), + weight_icon_sizes: new Array(30, 40, 50), + weight_icon_classes: new Array('marker-cluster-small', + 'marker-cluster-medium', + 'marker-cluster-large'), }; var settings = {}; /* @@ -121,19 +126,54 @@ See the file COPYING for details. } settings.icons = new Object(); if (settings.enable_clustering){ + settings.NumberedDivIcon = L.Icon.extend({ + options: { + iconUrl: STATIC_URL + 'chimere/img/empty.png', + number: '', + shadowUrl: null, + iconSize: new L.Point(40, 40), + iconAnchor: new L.Point(20, 20), + popupAnchor: new L.Point(0, -20), + className: '', + numberClassName: settings.weight_icon_classes[0]+'-number', + }, + + createIcon: function () { + var div = document.createElement('div'); + var img = this._createImg(this.options['iconUrl']); + var numdiv = document.createElement('div'); + numdiv.setAttribute( "class", this.options['numberClassName']); + numdiv.innerHTML = this.options['number'] || ''; + div.appendChild ( img ); + div.appendChild ( numdiv ); + this._setIconStyles(div, 'icon'); + return div; + }, + createShadow: function () { + return null; + } + }); + settings.layerMarkers = new L.MarkerClusterGroup({ - iconCreateFunction: function (cluster) { + iconCreateFunction : function (cluster) { var markers = cluster.getAllChildMarkers(); var weight = 0; for (idx=0;idx' + weight + '', + className: 'marker-cluster ' + settings.weight_icon_classes[idx], + iconSize: new L.Point(settings.weight_icon_sizes[idx], + settings.weight_icon_sizes[idx]) + }); + } }); } else { settings.layerMarkers = L.geoJson(null, { @@ -355,12 +395,29 @@ See the file COPYING for details. if (feature.properties.weight){ weight = feature.properties.weight; } + var idx = 2; + if (weight < settings.weight_steps[0]) { + idx = 0; + } else if (weight < settings.weight_steps[1]) { + idx = 1; + } + var size = settings.weight_icon_sizes[idx]; + var icon = new settings.NumberedDivIcon({ + number: weight, + iconSize: new L.Point(size, size), + iconAnchor: new L.Point(size/2, size/2), + popupAnchor: new L.Point(0, -size/2), + iconUrl:STATIC_URL + "chimere/img/" + settings.weight_icon_classes[idx] + ".png", + numberClassName:settings.weight_icon_classes[idx] + "-number" + }); +; var marker = new L.Marker( new L.LatLng( feature.geometry.coordinates[1], feature.geometry.coordinates[0]), { title: feature.properties.name, - weight: weight}); + weight: weight, + icon:icon}); marker.bindPopup(feature.properties.name); settings.layerMarkers.addLayer(marker); } -- cgit v1.2.3 From 04e654a925565f75a9efdb845de88d166bfbd76e Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 9 Apr 2013 01:58:59 +0200 Subject: Improve JSON generation performance --- chimere/models.py | 20 ++++++++++++++++++++ chimere/views.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/chimere/models.py b/chimere/models.py index c269439..7db530a 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -624,6 +624,19 @@ class Marker(GeographicItem): val = values[unicode(propertymodel.id)] self.setProperty(propertymodel, val) + def _getItems(self, base_dct={"properties":{}}): + '''Return a dict representation for json + ''' + item = base_dct + item["geometry"] = {"type": "Point", + "coordinates": [ self.point.x, self.point.y ] + } + item["properties"]['pk'] = self.pk + item["properties"]['name'] = self.name + if self.weight: + item["properties"]['weight'] = self.weight + return item + def getGeoJSON(self, categories_id=[]): '''Return a GeoJSON string ''' @@ -1117,6 +1130,13 @@ class Route(GeographicItem): properties.append(property) return properties + def _getItems(self, dct={'properties':{}}): + dct['geometry'] = { "type": "LineString", + "coordinates": [[point.x, point.y] + for point in self.route]} + dct['properties'].update({'id':self.id, 'name':self.name}) + return dct + def getGeoJSON(self, color="#000"): '''Return a GeoJSON string ''' diff --git a/chimere/views.py b/chimere/views.py index 7c02216..79d4ac2 100644 --- a/chimere/views.py +++ b/chimere/views.py @@ -604,10 +604,17 @@ def getGeoObjects(request, area_name, category_ids, status): idx = 0 current_cat = c_cat colors = list(Color.objects.filter(color_theme = c_cat.color_theme)) + color = '000' if colors: - jsons.append(route.getGeoJSON(color=colors[idx % len(colors)].code)) - else: - jsons.append(route.getGeoJSON(color='000')) + color = colors[idx % len(colors)].code + if '#' not in color: + color = '#' + color + base_dct = {"type":"Feature", + "properties":{ + "color":color + } + } + jsons.append(route._getItems(base_dct)) idx += 1 try: q = checkDate(Q(status__in=status, categories__in=category_ids)) @@ -615,10 +622,26 @@ def getGeoObjects(request, area_name, category_ids, status): except: return HttpResponse('no results') category_ids = [int(cat_id) for cat_id in category_ids] - jsons += [geo_object.getGeoJSON(category_ids) for geo_object in list(query)] + for category_id in category_ids: + cat = SubCategory.objects.get(pk=category_id) + base_dct = {"type":"Feature", + "properties":{ + "icon_path":unicode(cat.icon.image), + "icon_hover_path":cat.hover_icon.image \ + if cat.hover_icon else '', + "icon_width":cat.icon.image.width, + 'icon_height':cat.icon.image.height, + 'category_name':cat.name} + } + for obj in query.filter(categories__pk=category_id).all(): + dct = base_dct.copy() + dct['properties'] = base_dct['properties'].copy() + jsons.append(obj._getItems(base_dct=dct)) if not jsons: return HttpResponse('no results') - data = '{"type": "FeatureCollection", "features":[%s]}' % ",".join(jsons) + data = {"type": "FeatureCollection", "features":jsons} + data = json.dumps(data) + return HttpResponse(data) def get_all_categories(request, area_name=None): -- cgit v1.2.3 From c0c80a3e921fd69b6fa9950356fb7c12a1b15df4 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 9 Apr 2013 01:59:20 +0200 Subject: Improve leaflet performance --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index ef156fb..5cbeaac 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -155,6 +155,8 @@ See the file COPYING for details. }); settings.layerMarkers = new L.MarkerClusterGroup({ + spiderfyOnMaxZoom: false, showCoverageOnHover: false, + maxClusterRadius:50, iconCreateFunction : function (cluster) { var markers = cluster.getAllChildMarkers(); var weight = 0; @@ -385,6 +387,8 @@ See the file COPYING for details. $.ajax({url: uri, dataType: "json", success: function (data) { + settings.map.removeLayer(settings.layerMarkers); + settings.map.removeLayer(settings.layerVectors); settings.layerMarkers.clearLayers(); settings.layerVectors.clearLayers(); if (settings.enable_clustering){ @@ -410,7 +414,6 @@ See the file COPYING for details. iconUrl:STATIC_URL + "chimere/img/" + settings.weight_icon_classes[idx] + ".png", numberClassName:settings.weight_icon_classes[idx] + "-number" }); -; var marker = new L.Marker( new L.LatLng( feature.geometry.coordinates[1], @@ -430,6 +433,8 @@ See the file COPYING for details. } else { settings.layerMarkers.addData(data); } + settings.map.addLayer(settings.layerMarkers); + settings.map.addLayer(settings.layerVectors); }, error: function (data, textStatus, errorThrown) { settings.layerMarkers.clearLayers(); -- cgit v1.2.3 From aaa90f1194646386344b1d5ca2f83a609beee17f Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Thu, 11 Apr 2013 12:10:57 +0200 Subject: Manage object with not category in getGeoObjects --- chimere/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chimere/views.py b/chimere/views.py index 79d4ac2..136f34a 100644 --- a/chimere/views.py +++ b/chimere/views.py @@ -623,6 +623,8 @@ def getGeoObjects(request, area_name, category_ids, status): return HttpResponse('no results') category_ids = [int(cat_id) for cat_id in category_ids] for category_id in category_ids: + if not category_id: + continue cat = SubCategory.objects.get(pk=category_id) base_dct = {"type":"Feature", "properties":{ -- cgit v1.2.3 From 5cb7e1f3ce32a18b868453ccc6f3fb7fea04b876 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 22 Apr 2013 22:51:34 +0200 Subject: Add more cluster levels --- chimere/src/marker-cluster-large.xcf | Bin 0 -> 4179 bytes chimere/src/marker-cluster-medium.xcf | Bin 0 -> 2883 bytes chimere/src/marker-cluster-small.xcf | Bin 0 -> 2423 bytes chimere/src/marker-cluster-xlarge.xcf | Bin 0 -> 3125 bytes chimere/src/marker-cluster-xsmall.xcf | Bin 0 -> 2407 bytes chimere/src/marker-cluster-xxlarge.xcf | Bin 0 -> 4257 bytes chimere/src/share-icon.xcf | Bin 0 -> 5431 bytes chimere/static/chimere/css/styles.css | 120 +++++++++++++++++++-- .../static/chimere/img/marker-cluster-large.png | Bin 1182 -> 1115 bytes .../static/chimere/img/marker-cluster-small.png | Bin 701 -> 2067 bytes .../static/chimere/img/marker-cluster-xlarge.png | Bin 0 -> 1182 bytes .../static/chimere/img/marker-cluster-xsmall.png | Bin 0 -> 693 bytes .../static/chimere/img/marker-cluster-xxlarge.png | Bin 0 -> 1364 bytes chimere/static/chimere/img/share-icon.xcf | Bin 5431 -> 0 bytes .../static/chimere/js/jquery.chimere-leaflet.js | 12 ++- 15 files changed, 118 insertions(+), 14 deletions(-) create mode 100755 chimere/src/marker-cluster-large.xcf create mode 100755 chimere/src/marker-cluster-medium.xcf create mode 100755 chimere/src/marker-cluster-small.xcf create mode 100755 chimere/src/marker-cluster-xlarge.xcf create mode 100755 chimere/src/marker-cluster-xsmall.xcf create mode 100755 chimere/src/marker-cluster-xxlarge.xcf create mode 100755 chimere/src/share-icon.xcf create mode 100644 chimere/static/chimere/img/marker-cluster-xlarge.png create mode 100644 chimere/static/chimere/img/marker-cluster-xsmall.png create mode 100644 chimere/static/chimere/img/marker-cluster-xxlarge.png delete mode 100644 chimere/static/chimere/img/share-icon.xcf diff --git a/chimere/src/marker-cluster-large.xcf b/chimere/src/marker-cluster-large.xcf new file mode 100755 index 0000000..ac0e14d Binary files /dev/null and b/chimere/src/marker-cluster-large.xcf differ diff --git a/chimere/src/marker-cluster-medium.xcf b/chimere/src/marker-cluster-medium.xcf new file mode 100755 index 0000000..85de110 Binary files /dev/null and b/chimere/src/marker-cluster-medium.xcf differ diff --git a/chimere/src/marker-cluster-small.xcf b/chimere/src/marker-cluster-small.xcf new file mode 100755 index 0000000..3e38c15 Binary files /dev/null and b/chimere/src/marker-cluster-small.xcf differ diff --git a/chimere/src/marker-cluster-xlarge.xcf b/chimere/src/marker-cluster-xlarge.xcf new file mode 100755 index 0000000..6c9c890 Binary files /dev/null and b/chimere/src/marker-cluster-xlarge.xcf differ diff --git a/chimere/src/marker-cluster-xsmall.xcf b/chimere/src/marker-cluster-xsmall.xcf new file mode 100755 index 0000000..f0787cc Binary files /dev/null and b/chimere/src/marker-cluster-xsmall.xcf differ diff --git a/chimere/src/marker-cluster-xxlarge.xcf b/chimere/src/marker-cluster-xxlarge.xcf new file mode 100755 index 0000000..4c9be7c Binary files /dev/null and b/chimere/src/marker-cluster-xxlarge.xcf differ diff --git a/chimere/src/share-icon.xcf b/chimere/src/share-icon.xcf new file mode 100755 index 0000000..d149768 Binary files /dev/null and b/chimere/src/share-icon.xcf differ diff --git a/chimere/static/chimere/css/styles.css b/chimere/static/chimere/css/styles.css index a123a5d..b9c9975 100644 --- a/chimere/static/chimere/css/styles.css +++ b/chimere/static/chimere/css/styles.css @@ -1110,45 +1110,110 @@ div.pp_default .pp_expand{ bottom:5px; } -.marker-cluster.marker-cluster-large { +.marker-cluster.marker-cluster-xxlarge { + border-radius: 27px; +} + +.marker-cluster.marker-cluster-xlarge { border-radius: 25px; } +.marker-cluster.marker-cluster-large { + border-radius: 22px; +} + +.marker-cluster.marker-cluster-medium { + border-radius: 20px; +} + .marker-cluster.marker-cluster-small { + border-radius: 17px; +} + +.marker-cluster.marker-cluster-xsmall { border-radius: 15px; } -.marker-cluster.marker-cluster-large div { +.marker-cluster.marker-cluster-xxlarge div { + width: 44px; + height: 44px; + border-radius: 22px; +} + +.marker-cluster.marker-cluster-xlarge div { width: 40px; height: 40px; border-radius: 20px; } +.marker-cluster.marker-cluster-large div { + width: 34px; + height: 34px; + border-radius: 17px; +} + +.marker-cluster.marker-cluster-medium div { + width: 30px; + height: 30px; + border-radius: 15px; +} + .marker-cluster.marker-cluster-small div { + width: 24px; + height: 24px; + border-radius: 12px; +} + +.marker-cluster.marker-cluster-xsmall div { width: 20px; height: 20px; border-radius: 10px; } -.marker-cluster.marker-cluster-large span { +.marker-cluster.marker-cluster-xxlarge span { + line-height: 44px; +} + +.marker-cluster.marker-cluster-xlarge span { line-height: 40px; } +.marker-cluster.marker-cluster-large span { + line-height: 34px; +} + .marker-cluster.marker-cluster-small span { + line-height: 24px; +} + +.marker-cluster.marker-cluster-xsmall span { line-height: 20px; } +.marker-cluster-xsmall-number, .marker-cluster-small-number, .marker-cluster-medium-number, -.marker-cluster-large-number{ +.marker-cluster-large-number +.marker-cluster-xlarge-number +.marker-cluster-xxlarge-number{ position: relative; font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; text-align: center; } -.marker-cluster-small-number{ - width: 30px; - top: -22px; +.marker-cluster-xxlarge-number{ + width: 55px; + top: -35px; +} + +.marker-cluster-xlarge-number{ + width: 50px; + top: -32px; +} + +.marker-cluster-large-number{ + width: 45px; + top: -30px; } .marker-cluster-medium-number{ @@ -1156,7 +1221,42 @@ div.pp_default .pp_expand{ top: -27px; } -.marker-cluster-large-number{ - width: 50px; - top: -32px; +.marker-cluster-small-number{ + width: 35px; + top: -25px; +} + +.marker-cluster-xsmall-number{ + width: 30px; + top: -22px; +} + +.marker-cluster-xsmall { + background-color: rgba(221, 255, 180, 0.6); +} + +.marker-cluster-xsmall div { + background-color: rgba(150, 244, 97, 0.6); +} + +.marker-cluster-large { + background-color: rgba(255, 214, 140, 0.6); } +.marker-cluster-large div { + background-color: rgba(255, 183, 54, 0.6); +} + +.marker-cluster-xlarge { + background-color: rgba(253, 156, 115, 0.6); +} +.marker-cluster-xlarge div { + background-color: rgba(241, 128, 23, 0.6); +} + +.marker-cluster-xxlarge { + background-color: rgba(255, 174, 176, 0.6); +} +.marker-cluster-xxlarge div { + background-color: rgba(255, 48, 54, 0.6); +} + diff --git a/chimere/static/chimere/img/marker-cluster-large.png b/chimere/static/chimere/img/marker-cluster-large.png index 477e2b8..eef6a73 100644 Binary files a/chimere/static/chimere/img/marker-cluster-large.png and b/chimere/static/chimere/img/marker-cluster-large.png differ diff --git a/chimere/static/chimere/img/marker-cluster-small.png b/chimere/static/chimere/img/marker-cluster-small.png index ae5ce5a..0620694 100644 Binary files a/chimere/static/chimere/img/marker-cluster-small.png and b/chimere/static/chimere/img/marker-cluster-small.png differ diff --git a/chimere/static/chimere/img/marker-cluster-xlarge.png b/chimere/static/chimere/img/marker-cluster-xlarge.png new file mode 100644 index 0000000..477e2b8 Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-xlarge.png differ diff --git a/chimere/static/chimere/img/marker-cluster-xsmall.png b/chimere/static/chimere/img/marker-cluster-xsmall.png new file mode 100644 index 0000000..748b3ed Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-xsmall.png differ diff --git a/chimere/static/chimere/img/marker-cluster-xxlarge.png b/chimere/static/chimere/img/marker-cluster-xxlarge.png new file mode 100644 index 0000000..d7b435f Binary files /dev/null and b/chimere/static/chimere/img/marker-cluster-xxlarge.png differ diff --git a/chimere/static/chimere/img/share-icon.xcf b/chimere/static/chimere/img/share-icon.xcf deleted file mode 100644 index d149768..0000000 Binary files a/chimere/static/chimere/img/share-icon.xcf and /dev/null differ diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 5cbeaac..0b417e5 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -96,11 +96,15 @@ See the file COPYING for details. icon_start: null, icon_step: null, icon_end: null, - weight_steps: new Array(6, 15), - weight_icon_sizes: new Array(30, 40, 50), - weight_icon_classes: new Array('marker-cluster-small', + weight_steps: new Array(10, 50, 100, 500, 1000), + weight_icon_sizes: new Array(30, 34, 40, 44, 50, 54), + weight_icon_classes: new Array('marker-cluster-xsmall', + 'marker-cluster-small', 'marker-cluster-medium', - 'marker-cluster-large'), + 'marker-cluster-large', + 'marker-cluster-xlarge', + 'marker-cluster-xxlarge' + ) }; var settings = {}; /* -- cgit v1.2.3 From 2c22c60e8ac7670b8224b1e6c22dba7ff978351d Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 22 Apr 2013 23:10:14 +0200 Subject: Fix cluster management with several levels --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 0b417e5..56ba696 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -167,11 +167,12 @@ See the file COPYING for details. for (idx=0;idx' + weight + '', -- cgit v1.2.3 From 2bd78502b51ddd57e24a881b59811ac1863f58f2 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Mon, 22 Apr 2013 23:58:44 +0200 Subject: Manage many layers with Leaflet --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 9 +++++++-- chimere/templates/chimere/blocks/map.html | 1 + chimere/templatetags/chimere_tags.py | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 56ba696..51bd00a 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -41,6 +41,7 @@ See the file COPYING for details. permalink_div: null, permalink: null, // OL Control, could be overrided map_layers: null, + map_layer_names: null, selected_map_layer: null, dynamic_categories: false, display_submited: false, @@ -120,9 +121,13 @@ See the file COPYING for details. var map_element = $(this).attr('id'); settings.map = map = L.map(map_element); - for (idx in settings.map_layers){ - map.addLayer(settings.map_layers[idx]); + map.addLayer(settings.map_layers[0]); + var map_layers = {}; + for (idx=0 ; idx < settings.map_layers.length ; idx++){ + map_layers[settings.map_layer_names[idx]] = settings.map_layers[idx]; + alert(map_layers[settings.map_layer_names[idx]]); } + L.control.layers(map_layers).addTo(map); if(settings.zoom && settings.lat && settings.lon){ map.setView([settings.lat, settings.lon], settings.zoom); } else { diff --git a/chimere/templates/chimere/blocks/map.html b/chimere/templates/chimere/blocks/map.html index 2614f39..2c225de 100644 --- a/chimere/templates/chimere/blocks/map.html +++ b/chimere/templates/chimere/blocks/map.html @@ -21,6 +21,7 @@ var chimere_init_options = {}; chimere_init_options["default_icon"] = '{{STATIC_URL}}img/marker-green.png'; chimere_init_options["map_layers"] = [{{js_map_layers|safe|escape}}]; + chimere_init_options["map_layer_names"] = [{{js_map_layer_names|safe|escape}}]; chimere_init_options['permalink_label'] = '{%trans "Permalink"%}'; chimere_init_options['permalink_element'] = document.getElementById('permalink'); chimere_init_options['routing'] = {{routing}}; diff --git a/chimere/templatetags/chimere_tags.py b/chimere/templatetags/chimere_tags.py index f180061..fd700d9 100644 --- a/chimere/templatetags/chimere_tags.py +++ b/chimere/templatetags/chimere_tags.py @@ -202,6 +202,8 @@ def map(context, map_id='map'): map_layers, default_area = get_map_layers(area_name) context_data['js_map_layers'] = ", ".join( [js for name, js, default in map_layers]) + context_data['js_map_layer_names'] = '"'+ '", "'.join( + [name for name, js, default in map_layers]) + '"' context_data['map_layers'] = map_layers if default_area: context_data['selected_map_layer'] = default_area -- cgit v1.2.3 From bb5d66ee68c4119c78d7a054180cce71be359fe4 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 23 Apr 2013 00:02:51 +0200 Subject: Remove debug --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 1 - 1 file changed, 1 deletion(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index 51bd00a..b48a54e 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -125,7 +125,6 @@ See the file COPYING for details. var map_layers = {}; for (idx=0 ; idx < settings.map_layers.length ; idx++){ map_layers[settings.map_layer_names[idx]] = settings.map_layers[idx]; - alert(map_layers[settings.map_layer_names[idx]]); } L.control.layers(map_layers).addTo(map); if(settings.zoom && settings.lat && settings.lon){ -- cgit v1.2.3 From 2860fd0cf740783bfe666ba969c023ccbce9ef5e Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 23 Apr 2013 00:27:33 +0200 Subject: Allow generalization of CSV export --- chimere/admin.py | 20 ++++++++++++-------- chimere/utils.py | 10 +++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/chimere/admin.py b/chimere/admin.py index c36fd26..d492a4c 100644 --- a/chimere/admin.py +++ b/chimere/admin.py @@ -84,14 +84,18 @@ def export_to_shapefile(modeladmin, request, queryset): return response export_to_shapefile.short_description = _(u"Export to Shapefile") -def export_to_csv(modeladmin, request, queryset): - u""" - Export data to CSV - """ - filename, result = CSVManager.export(queryset) - response = HttpResponse(result, mimetype='text/csv') - response['Content-Disposition'] = 'attachment; filename=%s' % filename - return response +def _export_to_csv(cols=[]): + def func(modeladmin, request, queryset): + u""" + Export data to CSV + """ + filename, result = CSVManager.export(queryset, cols=cols) + response = HttpResponse(result, mimetype='text/csv') + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + return func + +export_to_csv = _export_to_csv() export_to_csv.short_description = _(u"Export to CSV") def managed_modified(modeladmin, request, queryset): diff --git a/chimere/utils.py b/chimere/utils.py index f5bc44f..028da06 100644 --- a/chimere/utils.py +++ b/chimere/utils.py @@ -554,10 +554,11 @@ class CSVManager(ImportManager): return (new_item, updated_item, msg) @classmethod - def export(cls, queryset): + def export(cls, queryset, cols=[]): dct = {'description':unicode(datetime.date.today()), 'data':[]} cls_name = queryset.model.__name__.lower() - cols = list(cls.COLS) + if not cols: + cols = list(cls.COLS) for pm in queryset.model.all_properties(): cols.append((pm.name, pm.getAttrName(), pm.getAttrName()+'_set')) header = [col[0] for col in cols] @@ -568,7 +569,10 @@ class CSVManager(ImportManager): if callable(attr): data.append(attr(item)) else: - data.append(getattr(item, attr)) + v = getattr(item, attr) + if v == None: + v = '' + data.append(v) dct['data'].append(data) filename = unicode_normalize(settings.PROJECT_NAME + dct['description']\ + '.csv') -- cgit v1.2.3 From 2bfbae53d5f59cb38388070cc31ca12dbfe40487 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 23 Apr 2013 00:30:32 +0200 Subject: Minor fix on CSV export --- chimere/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chimere/utils.py b/chimere/utils.py index 028da06..f86e5db 100644 --- a/chimere/utils.py +++ b/chimere/utils.py @@ -559,8 +559,9 @@ class CSVManager(ImportManager): cls_name = queryset.model.__name__.lower() if not cols: cols = list(cls.COLS) - for pm in queryset.model.all_properties(): - cols.append((pm.name, pm.getAttrName(), pm.getAttrName()+'_set')) + if hasattr(queryset.model, 'all_properties'): + for pm in queryset.model.all_properties(): + cols.append((pm.name, pm.getAttrName(), pm.getAttrName()+'_set')) header = [col[0] for col in cols] dct['data'].append(header) for item in queryset.all(): -- cgit v1.2.3 From 1ffe63b5ab64a51dd7f3c3ddcb98e770b32b74d3 Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Tue, 23 Apr 2013 17:47:23 +0200 Subject: Allow to load static JSON overlay layers --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 18 ++++++++++++++++-- chimere/urls.py | 4 +++- chimere/views.py | 4 ++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index b48a54e..f0ae75f 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -105,7 +105,8 @@ See the file COPYING for details. 'marker-cluster-large', 'marker-cluster-xlarge', 'marker-cluster-xxlarge' - ) + ), + extra_json_data: null }; var settings = {}; /* @@ -126,12 +127,13 @@ See the file COPYING for details. for (idx=0 ; idx < settings.map_layers.length ; idx++){ map_layers[settings.map_layer_names[idx]] = settings.map_layers[idx]; } - L.control.layers(map_layers).addTo(map); + settings.layer_control = L.control.layers(map_layers).addTo(map); if(settings.zoom && settings.lat && settings.lon){ map.setView([settings.lat, settings.lon], settings.zoom); } else { map.fitWorld(); } + settings.icons = new Object(); if (settings.enable_clustering){ settings.NumberedDivIcon = L.Icon.extend({ @@ -246,6 +248,18 @@ See the file COPYING for details. methods.loadCategories(); methods.loadGeoObjects(); + if (settings.extra_json_data){ + for (idx=0;idx +# Copyright (C) 2008-2013 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as @@ -103,6 +103,8 @@ urlpatterns += patterns('chimere.views', 'processRouteFile', name='process_route_file'), url(r'^(?P[a-zA-Z0-9_-]+/)?dyn/(?P\w+)/$', 'extraPage', name='extra_page'), + url(r'^(?P[a-zA-Z0-9_-]+/)?json/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+).json$', 'get_json', + name='get-json'), # At the end, because it catches large url(r'^(?P[a-zA-Z0-9_-]+)?', 'index', name="index"), ) diff --git a/chimere/views.py b/chimere/views.py index 136f34a..f58b7ec 100644 --- a/chimere/views.py +++ b/chimere/views.py @@ -828,6 +828,10 @@ def route(request, area_name, lon1, lat1, lonlat_steps, lon2, lat2, message) return HttpResponse(data) +def get_json(request, area_name='', app_name='', filename=''): + return HttpResponse(open(settings.STATIC_ROOT+app_name+'/json/'+filename+'.json'), + 'application/javascript', status=200) + def rss(request, area_name=''): ''' Redirect to RSS subscription page -- cgit v1.2.3 From fc1182be7f97361f486c82ac61ff38a06371c53d Mon Sep 17 00:00:00 2001 From: Étienne Loks Date: Wed, 24 Apr 2013 01:52:42 +0200 Subject: Fix overlay layer loading when many are available --- chimere/static/chimere/js/jquery.chimere-leaflet.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index f0ae75f..c67a7a1 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -250,13 +250,17 @@ See the file COPYING for details. methods.loadGeoObjects(); if (settings.extra_json_data){ for (idx=0;idx Date: Sat, 15 Jun 2013 22:03:05 +0200 Subject: Refactoring: renaming Area to Map --- chimere/actions.py | 12 +- chimere/admin.py | 30 +- chimere/feeds.py | 14 +- chimere/forms.py | 20 +- chimere/migrations/0005_rename_area_to_map.py | 295 + chimere/models.py | 136 +- chimere/scripts/upgrade.py | 315 - chimere/static/chimere/css/styles.css | 20 +- chimere/static/chimere/js/base.js | 14 +- .../static/chimere/js/jquery.chimere-leaflet.js | 4 +- chimere/static/chimere/js/jquery.chimere-ol.js | 6 +- chimere/static/leaflet/leaflet-src.js | 8338 ++++++++++++++++++++ chimere/templates/chimere/base.html | 4 +- chimere/templates/chimere/blocks/areas.html | 26 +- .../chimere/blocks/areas_alternative.html | 11 - chimere/templates/chimere/blocks/head_chimere.html | 4 +- .../templates/chimere/blocks/live_coordinates.html | 2 +- chimere/templates/chimere/blocks/map.html | 10 +- chimere/templates/chimere/blocks/maps.html | 25 + .../templates/chimere/blocks/maps_alternative.html | 11 + chimere/templates/chimere/blocks/news.html | 2 +- chimere/templates/chimere/blocks/welcome.html | 2 +- chimere/templates/chimere/detail.html | 2 +- chimere/templates/chimere/feeds/rss.html | 10 +- chimere/templates/chimere/main_map.html | 6 +- chimere/templatetags/chimere_tags.py | 112 +- chimere/urls.py | 56 +- chimere/views.py | 202 +- chimere/widgets.py | 48 +- 29 files changed, 9033 insertions(+), 704 deletions(-) create mode 100644 chimere/migrations/0005_rename_area_to_map.py delete mode 100755 chimere/scripts/upgrade.py create mode 100644 chimere/static/leaflet/leaflet-src.js mode change 100644 => 120000 chimere/templates/chimere/blocks/areas.html delete mode 100644 chimere/templates/chimere/blocks/areas_alternative.html create mode 100644 chimere/templates/chimere/blocks/maps.html create mode 100644 chimere/templates/chimere/blocks/maps_alternative.html diff --git a/chimere/actions.py b/chimere/actions.py index 8ef5338..7a4b263 100644 --- a/chimere/actions.py +++ b/chimere/actions.py @@ -32,9 +32,9 @@ class Action: self.id, self.path, self.label = id, path, label self.extra_url_args, self.url = extra_url_args, None - def update_url(self, area_name): + def update_url(self, map_name): self.url = reverse(self.path, - args=[area_name + '/' if area_name else ''] + self.extra_url_args) + args=[map_name + '/' if map_name else ''] + self.extra_url_args) default_actions = [(Action('view', 'chimere:index', _('View')), []), (Action('contribute', 'chimere:edit', _('Contribute')), @@ -51,15 +51,15 @@ if settings.EMAIL_HOST: _('Contact us')), []),) -def actions(area_name=''): +def actions(map_name=''): acts = default_actions[:] for act, childs in default_actions: - act.update_url(area_name) + act.update_url(map_name) for child_act in childs: - child_act.update_url(area_name) + child_act.update_url(map_name) for page in Page.objects.filter(available=True).order_by('order'): act = Action(page.mnemonic, 'chimere:extra_page', page.title, [page.mnemonic]) - act.update_url(area_name) + act.update_url(map_name) acts.append((act, [])) return acts diff --git a/chimere/admin.py b/chimere/admin.py index d492a4c..f30c00b 100644 --- a/chimere/admin.py +++ b/chimere/admin.py @@ -36,14 +36,14 @@ try: except ImportError: pass -from chimere.forms import MarkerAdminForm, RouteAdminForm, AreaAdminForm,\ +from chimere.forms import MarkerAdminForm, RouteAdminForm, MapAdminForm,\ NewsAdminForm, CategoryAdminForm, ImporterAdminForm, OSMForm, \ PageAdminForm, PictureFileAdminForm, MultimediaFileAdminForm from chimere.models import Category, Icon, SubCategory, Marker, \ - PropertyModel, News, Route, Area, ColorTheme, Color, \ - MultimediaFile, PictureFile, Importer, Layer, AreaLayers,\ + PropertyModel, News, Route, Map, ColorTheme, Color, \ + MultimediaFile, PictureFile, Importer, Layer, MapLayers,\ PropertyModelChoice, MultimediaExtension, Page,\ - get_areas_for_user, get_users_by_area + get_maps_for_user, get_users_by_map from chimere.utils import unicode_normalize, ShapefileManager, KMLManager,\ CSVManager @@ -232,10 +232,10 @@ class MarkerAdmin(admin.ModelAdmin): def queryset(self, request): qs = self.model._default_manager.get_query_set() if not request.user.is_superuser: - areas = get_areas_for_user(request.user) + maps = get_maps_for_user(request.user) contained = Q() - for area in areas: - contained = contained | area.getIncludeMarker() + for map in maps: + contained = contained | map.getIncludeMarker() qs = qs.filter(contained) ordering = self.ordering or () if ordering: @@ -296,10 +296,10 @@ class RouteAdmin(MarkerAdmin): def queryset(self, request): qs = self.model._default_manager.get_query_set() if not request.user.is_superuser: - areas = get_areas_for_user(request.user) + maps = get_maps_for_user(request.user) contained = Q() - for area in areas: - contained = contained | area.getIncludeRoute() + for map in maps: + contained = contained | map.getIncludeRoute() qs = qs.filter(contained) ordering = self.ordering or () if ordering: @@ -314,14 +314,14 @@ class RouteAdmin(MarkerAdmin): Route.objects.filter(pk=item_id)) class LayerInline(admin.TabularInline): - model = AreaLayers + model = MapLayers extra = 1 -class AreaAdmin(admin.ModelAdmin): +class MapAdmin(admin.ModelAdmin): """ - Specialized the area field. + Specialized the map field. """ - form = AreaAdminForm + form = MapAdminForm exclude = ['upper_left_corner', 'lower_right_corner'] inlines = [LayerInline] list_display = ['name', 'order', 'available', 'default'] @@ -447,6 +447,6 @@ admin.site.register(Marker, MarkerAdmin) admin.site.register(Route, RouteAdmin) if not settings.CHIMERE_HIDE_PROPERTYMODEL: admin.site.register(PropertyModel, PropertyModelAdmin) -admin.site.register(Area, AreaAdmin) +admin.site.register(Map, MapAdmin) admin.site.register(ColorTheme, ColorThemeAdmin) admin.site.register(Layer) diff --git a/chimere/feeds.py b/chimere/feeds.py index 53444ac..2a62da3 100644 --- a/chimere/feeds.py +++ b/chimere/feeds.py @@ -29,7 +29,7 @@ from django.utils.translation import ugettext as _ from django.shortcuts import get_object_or_404 -from chimere.models import Category, SubCategory, Marker, Area +from chimere.models import Category, SubCategory, Marker, Map class BaseFeed(Feed): """ @@ -64,7 +64,7 @@ class LatestPOIsByCategory(BaseFeed): title_template = "chimere/feeds/rss_title.html" description_template = "chimere/feeds/rss_descr.html" - def get_object(self, request, category_id, area_name=''): + def get_object(self, request, category_id, map_name=''): return get_object_or_404(Category, id=category_id) def title(self, obj): @@ -99,7 +99,7 @@ class LatestPOIsBySubCategory(BaseFeed): title_template = "chimere/feeds/rss_title.html" description_template = "chimere/feeds/rss_descr.html" - def get_object(self, request, category_id, area_name=''): + def get_object(self, request, category_id, map_name=''): return get_object_or_404(SubCategory, id=category_id) def title(self, obj): @@ -149,7 +149,7 @@ class LatestPOIsByZone(BaseFeed): lower_right_lat = 0 lower_right_lon = 0 - def get_object(self, request, area, area_name=''): + def get_object(self, request, area, map_name=''): """ Get the extra url. Parameters are the coordinates of the zone (the upper left and lower right points) @@ -207,8 +207,8 @@ class LatestPOIsByZoneID(BaseFeed): title_template = "chimere/feeds/rss_title.html" description_template = "chimere/feeds/rss_descr.html" - def get_object(self, request, area_id, area_name=''): - return get_object_or_404(Area, id=area_id) + def get_object(self, request, map_id, map_name=''): + return get_object_or_404(Map, id=map_id) def title(self, obj): return settings.PROJECT_NAME + u" - " + \ @@ -217,7 +217,7 @@ class LatestPOIsByZoneID(BaseFeed): def link(self, obj): if not obj: raise FeedDoesNotExist - return reverse('chimere:feeds-areaid', args=['', obj.id]) + return reverse('chimere:feeds-mapid', args=['', obj.id]) def items(self, obj): q = Marker.objects.filter(available_date__isnull=False, status='A') diff --git a/chimere/forms.py b/chimere/forms.py index ea23f94..b6d5feb 100644 --- a/chimere/forms.py +++ b/chimere/forms.py @@ -31,7 +31,7 @@ from django.contrib.auth.models import User, Permission, ContentType from django.contrib.admin.widgets import AdminDateWidget from django.core.mail import EmailMessage, BadHeaderError -from chimere.models import Marker, Route, PropertyModel, Property, Area,\ +from chimere.models import Marker, Route, PropertyModel, Property, Map,\ News, Category, SubCategory, RouteFile, MultimediaFile, MultimediaType, \ PictureFile, Importer, PropertyModelChoice, IFRAME_LINKS, \ MultimediaExtension, Page, IMPORTER_CHOICES @@ -514,7 +514,7 @@ class FullFileForm(FileForm): super(FullFileForm, self).__init__(*args, **kwargs) self.fields.keyOrder = ['name', 'raw_file'] -class AreaAdminForm(forms.ModelForm): +class MapAdminForm(forms.ModelForm): """ Admin page to create an area """ @@ -522,7 +522,7 @@ class AreaAdminForm(forms.ModelForm): welcome_message = forms.CharField(widget=TextareaAdminWidget, required=False) class Meta: - model = Area + model = Map def __init__(self, *args, **keys): """ @@ -547,7 +547,7 @@ class AreaAdminForm(forms.ModelForm): keys['initial'].update(dct) else: keys['initial'] = dct - super(AreaAdminForm, self).__init__(*args, **keys) + super(MapAdminForm, self).__init__(*args, **keys) def clean(self): ''' @@ -561,7 +561,7 @@ class AreaAdminForm(forms.ModelForm): msg = _(u"No area selected.") raise forms.ValidationError(msg) if self.cleaned_data.get('order'): - q = Area.objects.filter(order=self.cleaned_data.get('order')) + q = Map.objects.filter(order=self.cleaned_data.get('order')) if self.instance: q = q.exclude(pk=self.instance.pk) if q.count(): @@ -574,7 +574,7 @@ class AreaAdminForm(forms.ModelForm): """ Custom save method in order to manage area """ - new_area = super(AreaAdminForm, self).save(*args, **keys) + new_area = super(MapAdminForm, self).save(*args, **keys) area = self.cleaned_data['area'] new_area.upper_left_corner = 'POINT(%s %s)' % (area[0][0], area[0][1]) new_area.lower_right_corner = 'POINT(%s %s)' % (area[1][0], @@ -582,7 +582,7 @@ class AreaAdminForm(forms.ModelForm): content_type = ContentType.objects.get(app_label="chimere", model="area") if new_area.urn: - mnemo = 'change_area_' + new_area.urn + mnemo = 'change_map_' + new_area.urn perm = Permission.objects.filter(codename=mnemo) if not perm: perm = Permission(name='Can change ' + new_area.name, @@ -590,18 +590,18 @@ class AreaAdminForm(forms.ModelForm): perm.save() else: if 'urn' in self.initial: - mnemo = 'change_area_' + self.initial['urn'] + mnemo = 'change_map_' + self.initial['urn'] perm = Permission.objects.filter(codename=mnemo) if perm: perm[0].delete() return new_area -class AreaForm(AreaAdminForm): +class MapForm(MapAdminForm): """ Form for the edit page """ class Meta: - model = Area + model = Map CHIMERE_ROUTING_TRANSPORT = [] ROUTING_INIT = None diff --git a/chimere/migrations/0005_rename_area_to_map.py b/chimere/migrations/0005_rename_area_to_map.py new file mode 100644 index 0000000..0a3af14 --- /dev/null +++ b/chimere/migrations/0005_rename_area_to_map.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + db.rename_table('chimere_area', 'chimere_map') + db.rename_table('chimere_arealayers', 'chimere_maplayers') + db.rename_column('chimere_maplayers', 'area_id', 'map_id') + db.rename_table('chimere_subcategory_areas', 'chimere_subcategory_maps') + db.rename_column('chimere_subcategory_maps', 'area_id', 'map_id') + db.rename_table('chimere_area_default_subcategories', + 'chimere_map_default_subcategories') + db.rename_column('chimere_map_default_subcategories', 'area_id', + 'map_id') + db.rename_table('chimere_news_areas', 'chimere_news_maps') + db.rename_column('chimere_news_maps', 'area_id', 'map_id') + + def backwards(self, orm): + db.rename_table('chimere_map', 'chimere_area') + db.rename_table('chimere_maplayers', 'chimere_arealayers') + db.rename_column('chimere_arealayers', 'map_id', 'area_id') + db.rename_table('chimere_subcategory_maps', 'chimere_subcategory_areas') + db.rename_column('chimere_subcategory_areas', 'map_id', 'area_id') + db.rename_table('chimere_map_default_subcategories', + 'chimere_area_default_subcategories') + db.rename_column('chimere_area_default_subcategories', 'map_id', + 'area_id') + db.rename_table('chimere_news_maps', + 'chimere_news_areas ') + db.rename_column('chimere_news_maps', 'map_id', 'area_id') + + models = { + 'chimere.aggregatedroute': { + 'Meta': {'object_name': 'AggregatedRoute', 'db_table': "'chimere_aggregated_routes'", 'managed': 'False'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'route': ('django.contrib.gis.db.models.fields.MultiLineStringField', [], {}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'subcategory': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.SubCategory']"}) + }, + 'chimere.category': { + 'Meta': {'ordering': "['order']", 'object_name': 'Category'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.color': { + 'Meta': {'ordering': "['order']", 'object_name': 'Color'}, + 'code': ('django.db.models.fields.CharField', [], {'max_length': '6'}), + 'color_theme': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'colors'", 'to': "orm['chimere.ColorTheme']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.colortheme': { + 'Meta': {'object_name': 'ColorTheme'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.icon': { + 'Meta': {'object_name': 'Icon'}, + 'height': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'width': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.importer': { + 'Meta': {'object_name': 'Importer'}, + 'associate_marker_to_way': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'default_description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'default_name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'filtr': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'get_description': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'importer_type': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'source_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'srid': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'zipped': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'chimere.layer': { + 'Meta': {'object_name': 'Layer'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer_code': ('django.db.models.fields.TextField', [], {'max_length': '300'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.map': { + 'Meta': {'ordering': "('order', 'name')", 'object_name': 'Map'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'default': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'default_subcategories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False', 'blank': 'True'}), + 'dynamic_categories': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'external_css': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layers': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'maps'", 'blank': 'True', 'through': "orm['chimere.MapLayers']", 'to': "orm['chimere.Layer']"}), + 'lower_right_corner': ('django.contrib.gis.db.models.fields.PointField', [], {'default': "'POINT(0 0)'"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'unique': 'True'}), + 'restrict_to_extent': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'subcategories': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'maps'", 'blank': 'True', 'db_table': "'chimere_subcategory_maps'", 'to': "orm['chimere.SubCategory']"}), + 'upper_left_corner': ('django.contrib.gis.db.models.fields.PointField', [], {'default': "'POINT(0 0)'"}), + 'urn': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'blank': 'True'}), + 'welcome_message': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.maplayers': { + 'Meta': {'ordering': "('order',)", 'object_name': 'MapLayers'}, + 'default': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'layer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Layer']"}), + 'map': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Map']"}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'chimere.marker': { + 'Meta': {'ordering': "('status', 'name')", 'object_name': 'Marker'}, + 'available_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'import_key': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_version': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'is_front_page': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'modified_since_import': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'not_for_osm': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'point': ('chimere.widgets.PointField', [], {}), + 'ref_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'submited_marker'", 'null': 'True', 'to': "orm['chimere.Marker']"}), + 'route': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'associated_marker'", 'null': 'True', 'to': "orm['chimere.Route']"}), + 'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'submiter_comment': ('django.db.models.fields.TextField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'submiter_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'submiter_name': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'submiter_session_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.multimediaextension': { + 'Meta': {'object_name': 'MultimediaExtension'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'multimedia_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'extensions'", 'to': "orm['chimere.MultimediaType']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '6'}) + }, + 'chimere.multimediafile': { + 'Meta': {'object_name': 'MultimediaFile'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'multimedia_files'", 'to': "orm['chimere.Marker']"}), + 'miniature': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'multimedia_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.MultimediaType']", 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'chimere.multimediatype': { + 'Meta': {'object_name': 'MultimediaType'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'iframe': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'mime_type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.news': { + 'Meta': {'object_name': 'News'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'date': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_front_page': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), + 'maps': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'to': "orm['chimere.Map']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.page': { + 'Meta': {'object_name': 'Page'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mnemonic': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '10', 'null': 'True', 'blank': 'True'}), + 'template_path': ('django.db.models.fields.CharField', [], {'max_length': '150', 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.picturefile': { + 'Meta': {'object_name': 'PictureFile'}, + 'height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pictures'", 'to': "orm['chimere.Marker']"}), + 'miniature': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'picture': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'thumbnailfile': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'thumbnailfile_height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'thumbnailfile_width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.property': { + 'Meta': {'object_name': 'Property'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marker': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Marker']"}), + 'propertymodel': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.PropertyModel']"}), + 'value': ('django.db.models.fields.TextField', [], {}) + }, + 'chimere.propertymodel': { + 'Meta': {'ordering': "('order',)", 'object_name': 'PropertyModel'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mandatory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {}), + 'subcategories': ('chimere.widgets.SelectMultipleField', [], {'symmetrical': 'False', 'related_name': "'properties'", 'blank': 'True', 'to': "orm['chimere.SubCategory']"}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '1'}) + }, + 'chimere.propertymodelchoice': { + 'Meta': {'object_name': 'PropertyModelChoice'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'propertymodel': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'choices'", 'to': "orm['chimere.PropertyModel']"}), + 'value': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + 'chimere.route': { + 'Meta': {'ordering': "('status', 'name')", 'object_name': 'Route'}, + 'associated_file': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.RouteFile']", 'null': 'True', 'blank': 'True'}), + 'categories': ('chimere.widgets.SelectMultipleField', [], {'to': "orm['chimere.SubCategory']", 'symmetrical': 'False'}), + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'has_associated_marker': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'import_key': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_source': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'import_version': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'license': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'modified_since_import': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'not_for_osm': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'origin': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'ref_item': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'submited_route'", 'null': 'True', 'to': "orm['chimere.Route']"}), + 'route': ('chimere.widgets.RouteField', [], {}), + 'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'submiter_comment': ('django.db.models.fields.TextField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'submiter_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'submiter_name': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'submiter_session_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'chimere.routefile': { + 'Meta': {'ordering': "('name',)", 'object_name': 'RouteFile'}, + 'file_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'raw_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'simplified_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}) + }, + 'chimere.subcategory': { + 'Meta': {'ordering': "['category', 'order']", 'object_name': 'SubCategory'}, + 'as_layer': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subcategories'", 'to': "orm['chimere.Category']"}), + 'color_theme': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.ColorTheme']", 'null': 'True', 'blank': 'True'}), + 'dated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'hover_icon': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'subcat_hovered'", 'null': 'True', 'to': "orm['chimere.Icon']"}), + 'icon': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['chimere.Icon']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'item_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1000'}), + 'routing_warn': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'submission': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'weighted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'chimere.tinyurl': { + 'Meta': {'object_name': 'TinyUrl'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'parameters': ('django.db.models.fields.CharField', [], {'max_length': '500'}) + } + } + + complete_apps = ['chimere'] diff --git a/chimere/models.py b/chimere/models.py index 7db530a..60e4cd1 100644 --- a/chimere/models.py +++ b/chimere/models.py @@ -105,7 +105,7 @@ class News(models.Model): date = models.DateField(_(u"Date"), auto_now_add=True) content = models.TextField() url = models.URLField(_(u"Url"), max_length=200, blank=True, null=True) - areas = SelectMultipleField('Area', verbose_name=_(u"Associated areas"), + maps = SelectMultipleField('Map', verbose_name=_(u"Associated maps"), blank=True, null=True) def __unicode__(self): ordering = ["-date"] @@ -239,7 +239,7 @@ class SubCategory(models.Model): verbose_name_plural = _(u"Sub-categories") @classmethod - def getAvailable(cls, item_types=None, area_name=None, public=False): + def getAvailable(cls, item_types=None, map_name=None, public=False): '''Get list of tuples with first the category and second the associated subcategories ''' @@ -252,14 +252,14 @@ class SubCategory(models.Model): if public: subcategories = subcategories.filter(submission=True) selected_cats = [] - if area_name: - area = Area.objects.get(urn=area_name) + if map_name: + map = Map.objects.get(urn=map_name) # if there some restrictions with categories limit them - if area.subcategories.count(): - sub_ids = [sub.id for sub in area.subcategories.all()] + if map.subcategories.count(): + sub_ids = [sub.id for sub in map.subcategories.all()] subcategories = subcategories.filter(id__in=sub_ids) selected_cats = [subcat.pk - for subcat in area.default_subcategories.all()] + for subcat in map.default_subcategories.all()] for sub_category in subcategories.order_by('order'): if sub_category.category not in sub_categories: sub_categories[sub_category.category] = [] @@ -275,9 +275,9 @@ class SubCategory(models.Model): return subcategories @classmethod - def getAvailableTuples(cls, area_name=None): + def getAvailableTuples(cls, map_name=None): cats = [] - for cat, subcats in cls.getAvailable(area_name=area_name): + for cat, subcats in cls.getAvailable(map_name=map_name): cats.append((unicode(cat), [(subcat.pk, subcat.name) for subcat in subcats])) return cats @@ -679,20 +679,20 @@ class Marker(GeographicItem): if cats.count(): return cats.all()[0] - def get_absolute_url(self, area_name=''): + def get_absolute_url(self, map_name=''): parameters = 'current_feature=%d' % self.id if self.default_category: parameters += '&checked_categories=%s' % self.default_category.pk urn = TinyUrl.getUrnByParameters(parameters) - area_name = area_name + '/' if area_name else '' - url = reverse('chimere:tiny', args=[area_name, urn]) + map_name = map_name + '/' if map_name else '' + url = reverse('chimere:tiny', args=[map_name, urn]) return url PRE_ATTRS = { 'Marker':('name', 'geometry', 'import_version', 'modified_since_import'), 'Route':('name', 'geometry', 'import_version', 'modified_since_import'), - 'Area':('urn', 'name'), + 'Map':('urn', 'name'), } def geometry_pre_save(cls, pre_save_geom_values): def geom_pre_save(sender, **kwargs): @@ -1278,14 +1278,14 @@ class SimpleArea: return True return False - def getCategories(self, status='A', filter_available=True, area_name=None): + def getCategories(self, status='A', filter_available=True, map_name=None): """ - Get categories for this area + Get categories for this map """ wheres = [] - if area_name: + if map_name: subcategory_pks = [] - for cat, subcats in SubCategory.getAvailable(area_name=area_name): + for cat, subcats in SubCategory.getAvailable(map_name=map_name): for subcat in subcats: subcategory_pks.append(unicode(subcat.pk)) if filter_available: @@ -1349,11 +1349,11 @@ class Layer(models.Model): class Meta: verbose_name = _("Layer") -class Area(models.Model, SimpleArea): - """Rectangular area of the map +class Map(models.Model, SimpleArea): + """A map """ name = models.CharField(_(u"Name"), max_length=150) - urn = models.SlugField(_(u"Area urn"), max_length=50, blank=True, + urn = models.SlugField(_(u"Map urn"), max_length=50, blank=True, unique=True) welcome_message = models.TextField(_(u"Welcome message"), blank=True, null=True) @@ -1363,18 +1363,18 @@ class Area(models.Model, SimpleArea): default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) lower_right_corner = models.PointField(_(u"Lower right corner"), default='POINT(0 0)', srid=settings.CHIMERE_EPSG_DISPLAY_PROJECTION) - default = models.NullBooleanField(_(u"Default area"), - help_text=_(u"Only one area is set by default")) - layers = SelectMultipleField(Layer, related_name='areas', - through='AreaLayers', blank=True) + default = models.NullBooleanField(_(u"Default map"), + help_text=_(u"Only one map is set by default")) + layers = SelectMultipleField(Layer, related_name='maps', + through='MapLayers', blank=True) default_subcategories = SelectMultipleField(SubCategory, blank=True, verbose_name=_(u"Sub-categories checked by default")) dynamic_categories = models.NullBooleanField( _(u"Sub-categories dynamicaly displayed"), help_text=_(u"If checked, categories are only displayed in the menu if " u"they are available on the current extent.")) - subcategories = SelectMultipleField(SubCategory, related_name='areas', - blank=True, db_table='chimere_subcategory_areas', + subcategories = SelectMultipleField(SubCategory, related_name='maps', + blank=True, db_table='chimere_subcategory_maps', verbose_name=_(u"Restricted to theses sub-categories"), help_text=_(u"If no sub-category is set all sub-categories are " u"available")) @@ -1389,11 +1389,11 @@ class Area(models.Model, SimpleArea): class Meta: ordering = ('order', 'name') - verbose_name = _("Area") + verbose_name = _("Map") @classmethod def getAvailable(cls): - '''Get available areas + '''Get available maps ''' return cls.objects.filter(available=True) @@ -1419,54 +1419,64 @@ class Area(models.Model, SimpleArea): """ return Q(route__contained=self.getWkt()) -pre_save_area_values = {} -def area_pre_save(sender, **kwargs): +pre_save_map_values = {} +def map_pre_save(sender, **kwargs): if not kwargs['instance']: return - geometry_pre_save(Area, pre_save_area_values)(sender, **kwargs) -pre_save.connect(area_pre_save, sender=Area) + geometry_pre_save(Map, pre_save_map_values)(sender, **kwargs) +pre_save.connect(map_pre_save, sender=Map) -def area_post_save(sender, **kwargs): +def map_post_save(sender, **kwargs): if not kwargs['instance']: return - area = kwargs['instance'] - if area.default: - defaults = Area.objects.filter(default=True).exclude(pk=area.pk) + map = kwargs['instance'] + if map.default: + defaults = Map.objects.filter(default=True).exclude(pk=map.pk) for default in defaults: default.default = False default.save() # manage permissions - old_urn, old_name = area.urn, area.name - if area.pk in pre_save_area_values: - old_urn, old_name = pre_save_area_values[area.pk] + old_urn, old_name = map.urn, map.name + if map.pk in pre_save_map_values: + old_urn, old_name = pre_save_map_values[map.pk] perm, old_groups, old_users = None, [], [] - if area.urn != old_urn: - oldmnemo = 'change_area_' + old_urn + + if map.urn != old_urn: + oldmnemo = 'change_map_' + old_urn old_perm = Permission.objects.filter(codename=oldmnemo) if old_perm.count(): perm = old_perm.all()[0] - perm.codename = 'change_area_' + area.urn + perm.codename = 'change_map_' + map.urn perm.save() - if not area.urn: - area.urn = defaultfilters.slugify(area.name) - area.save() - mnemo = 'change_area_' + area.urn + if not map.urn: + map.urn = defaultfilters.slugify(map.name) + map.save() + + # fix old mnemo + oldmnemo = 'change_area_' + old_urn + old_perm = Permission.objects.filter(codename=oldmnemo) + if old_perm.count(): + perm = old_perm.all()[0] + perm.codename = 'change_map_' + map.urn + perm.save() + + mnemo = 'change_map_' + map.urn perm = Permission.objects.filter(codename=mnemo) - lbl = "Can change " + area.name + lbl = "Can change " + map.name if not perm.count(): content_type, created = ContentType.objects.get_or_create( - app_label="chimere", model="area") + app_label="chimere", model="map") perm = Permission(name=lbl, content_type_id=content_type.id, codename=mnemo) perm.save() else: perm = perm.all()[0] - if old_name != area.name: + if old_name != map.name: perm.name = lbl perm.save() # manage moderation group - groupname = area.name + " moderation" - if old_name != area.name: + groupname = map.name + " moderation" + if old_name != map.name: old_groupname = old_name + " moderation" old_gp = Group.objects.filter(name=old_groupname) if old_gp.count(): @@ -1487,33 +1497,33 @@ def area_post_save(sender, **kwargs): for p in Permission.objects.filter(content_type=ct).all(): group.permissions.add(p) -post_save.connect(area_post_save, sender=Area) +post_save.connect(map_post_save, sender=Map) -def get_areas_for_user(user): +def get_maps_for_user(user): """ Getting subcats for a specific user """ perms = user.get_all_permissions() - areas = set() - prefix = 'chimere.change_area_' + maps = set() + prefix = 'chimere.change_map_' for perm in perms: if perm.startswith(prefix): try: - area = Area.objects.get(urn=perm[len(prefix):]) - areas.add(area) + map = Map.objects.get(urn=perm[len(prefix):]) + maps.add(map) except ObjectDoesNotExist: pass - return areas + return maps -def get_users_by_area(area): - if not area: +def get_users_by_map(map): + if not map: return [] - perm = 'change_area_'+area.urn + perm = 'change_map_'+map.urn return User.objects.filter(Q(groups__permissions__codename=perm)| Q(user_permissions__codename=perm)).all() -class AreaLayers(models.Model): - area = models.ForeignKey(Area) +class MapLayers(models.Model): + map = models.ForeignKey(Map) layer = models.ForeignKey(Layer) order = models.IntegerField(_(u"Order")) default = models.NullBooleanField(_(u"Default layer")) diff --git a/chimere/scripts/upgrade.py b/chimere/scripts/upgrade.py deleted file mode 100755 index accb751..0000000 --- a/chimere/scripts/upgrade.py +++ /dev/null @@ -1,315 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -import sys -sys.path.append('.') - -from django.core.management import setup_environ -import settings - -setup_environ(settings) - -from django.db import connection, transaction - -cursor = connection.cursor() - -from main.models import Area, Marker, Route, Icon, SubCategory -from django.contrib.gis.geos import LineString - -# early versions before 0.1: urn field doesn't exist for area - -import htmlentitydefs, re - -def slugfy(text, separator): - ret = u"" - text = text.strip() - for c in text.lower(): - try: - ret += htmlentitydefs.codepoint2name[ord(c)][0] - except: - ret += c - ret = re.sub("\W", " ", ret) - ret = re.sub(" +", separator, ret) - return ret.strip() - -QUERY_CHECK_FIELD = """SELECT a.attname AS field FROM pg_class c, pg_attribute a - WHERE c.relname = '%s' AND a.attnum > 0 AND a.attrelid = c.oid - AND a.attname='%s';""" -QUERY_CHECK_TABLE = """SELECT c.relname FROM pg_class c -WHERE c.relname = '%s';""" - - -query = QUERY_CHECK_FIELD % ('main_area', 'urn') -cursor.execute(query) -transaction.commit_unless_managed() - -row = cursor.fetchone() -if not row: - query_update = "ALTER TABLE main_area ADD COLUMN urn VARCHAR(50) \ -UNIQUE" - cursor.execute(query_update) - transaction.commit_unless_managed() - areas = Area.objects.all() - print " * urn field created in table main_area" - for area in areas: - urn = slugfy(area.name, "-") - area.urn = urn - area.save() - print " * area %s urn is now: %s" % (area.name, area.urn) - - query = "ALTER TABLE main_area ALTER COLUMN urn SET not null;" - cursor.execute(query) - transaction.commit_unless_managed() - print " * urn field has now the constraint NOT NULL" - -from django.contrib.auth.models import Permission, ContentType - -# check if permission have been correctly set for each areas -areas = Area.objects.all() -for area in areas: - content_type = ContentType.objects.get(app_label="main", - model="area") - mnemo = 'change_area_' + area.urn - perm = Permission.objects.filter(codename=mnemo) - if not perm: - lbl = "Can change " + area.name - perm = Permission(name=lbl, content_type_id=content_type.id, - codename=mnemo) - perm.save() - print ' * permission "' + lbl + '" has been created' - print " WARNING: don't forget to update administrator's rights with \ -this new permission" - -# early versions before 0.1: subcategory_areas table doesn't exist -# version 1.0 subcategory_areas renamed to main_subcategory_areas - -query = QUERY_CHECK_TABLE % 'main_subcategory_areas' -cursor.execute(query) -transaction.commit_unless_managed() - -row = cursor.fetchone() -if not row: - query = QUERY_CHECK_TABLE % 'subcategory_areas' - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if row: - query_rename = "ALTER TABLE subcategory_areas RENAME TO \ -main_subcategory_areas;" - cursor.execute(query_rename) - transaction.commit_unless_managed() - print " * subcategory_areas renamed to main_subcategory_areas" - else: - query_create = """ -CREATE TABLE "main_subcategory_areas" ( - "id" serial NOT NULL PRIMARY KEY, - "subcategory_id" integer NOT NULL REFERENCES "main_subcategory" ("id") - DEFERRABLE INITIALLY DEFERRED, - "area_id" integer NOT NULL REFERENCES "main_area" ("id") - DEFERRABLE INITIALLY DEFERRED, - UNIQUE ("subcategory_id", "area_id")); -""" - cursor.execute(query_create) - transaction.commit_unless_managed() - print " * main_subcategory_areas created" - -# early versions before 0.1: main_tinyurl table doesn't exist - -query = QUERY_CHECK_TABLE % 'main_tinyurl' -cursor.execute(query) -transaction.commit_unless_managed() - -row = cursor.fetchone() -if not row: - query_create = """ -CREATE TABLE "main_tinyurl" ( - "id" serial NOT NULL PRIMARY KEY, - "parameters" varchar(500) NOT NULL); -""" - cursor.execute(query_create) - transaction.commit_unless_managed() - print " * main_tinyurl created" - - -# early versions before 0.1: save area with wrong SRID -# only errors with default SRID is managed adapt the script for your SRID - -from osgeo import osr - -srs = osr.SpatialReference() -srs.ImportFromEPSG(4326) # WGS84 -ll = srs.CloneGeogCS() -srs.ImportFromEPSG(settings.EPSG_PROJECTION) -proj = osr.CoordinateTransformation(srs, ll) - -changed = False -areas = Area.objects.all() -for area in areas: - # only one test: assume each point as been save with the same SRID... - if area.upper_left_corner.srid == 4326 and area.upper_left_corner.x > 90 \ - or area.upper_left_corner.x < -90: - changed = True - pt = proj.TransformPoint(area.upper_left_corner.y, - area.upper_left_corner.x) - area.upper_left_corner.x = pt[0] - area.upper_left_corner.y = pt[1] - pt = proj.TransformPoint(area.lower_right_corner.y, - area.lower_right_corner.x) - area.lower_right_corner.x = pt[0] - area.lower_right_corner.y = pt[1] - area.save() -if changed: - print " * projections of areas corrected" - -# changement from version 1.0 to 1.1: add dated fields to markers and routes -if settings.DAYS_BEFORE_EVENT: - for cls in (Marker, Route): - table = cls._meta.db_table - query = QUERY_CHECK_FIELD % (table, 'start_date') - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if not row: - query_update = "ALTER TABLE "+table+" ADD COLUMN start_date date" - cursor.execute(query_update) - transaction.commit_unless_managed() - query_update = "ALTER TABLE "+table+" ADD COLUMN end_date date" - cursor.execute(query_update) - transaction.commit_unless_managed() - print " * start_date and end_date added to table " + table + "." - -# changement from version 1.0 to 1.1: add available_date field to marker -if 'chimere.rss' in settings.INSTALLED_APPS: - for cls in (Marker,): - table = cls._meta.db_table - query = QUERY_CHECK_FIELD % (table, 'available_date') - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if not row: - query_update = "ALTER TABLE " + table + " ADD COLUMN \ -available_date timestamp with time zone" - cursor.execute(query_update) - transaction.commit_unless_managed() - print " * available_date added to table " + table + "." - -# changement from version 1.0 to 1.1: version of django 1.2 -# create specific height and width for image fields - -for cls, attr in ((Icon, "image"), (Marker, "picture"), - (Route, "picture")): - table = cls._meta.db_table - query = QUERY_CHECK_FIELD % (table, 'width') - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if not row: - query_update = "ALTER TABLE "+table+" ADD COLUMN width integer" - cursor.execute(query_update) - transaction.commit_unless_managed() - query_update = "ALTER TABLE "+table+" ADD COLUMN height integer" - cursor.execute(query_update) - transaction.commit_unless_managed() - for obj in cls.objects.all(): - image = getattr(obj, attr) - if not image: - continue - obj.width = image.width - obj.height = image.height - obj.save() - print " * height and width of " + table + " corrected" - -# changement from version 1.0 to 1.1: multiple selection of categories - -for cls in (Marker, Route): - table = cls._meta.db_table[len("main_"):] - query = QUERY_CHECK_TABLE % ('main_' + table + '_categories') - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if row: - continue - query_create = """ -CREATE TABLE "main_%s_categories" ( - "id" serial NOT NULL PRIMARY KEY, - "%s_id" integer NOT NULL REFERENCES "main_%s" ("id") DEFERRABLE INITIALLY DEFERRED, - "subcategory_id" integer NOT NULL REFERENCES "main_subcategory" ("id") DEFERRABLE INITIALLY DEFERRED, - UNIQUE ("%s_id", "subcategory_id")); -""" % (table, table, table, table) - cursor.execute(query_create) - transaction.commit_unless_managed() - for obj in cls.objects.all(): - query = "select subcategory_id from main_%s where id=%d" % (table, - obj.id) - cursor.execute(query) - transaction.commit_unless_managed() - - row = cursor.fetchone() - if row: - obj.categories.add(SubCategory.objects.get(id=row[0])) - obj.save() - query = "ALTER TABLE main_%s DROP COLUMN subcategory_id;" % table - cursor.execute(query) - transaction.commit_unless_managed() - print " * main_%s_categories created" % table - -# -> version 1.2: associate point to route (for the future) -query = QUERY_CHECK_FIELD % ('main_marker', 'route_id') -cursor.execute(query) -transaction.commit_unless_managed() - -row = cursor.fetchone() -if not row: - query_update = 'ALTER TABLE "main_marker" ADD COLUMN \ -"route_id" integer REFERENCES "main_route" ("id") DEFERRABLE INITIALLY DEFERRED' - cursor.execute(query_update) - transaction.commit_unless_managed() - print " * route_id added to table main_marker." - -# -> version 1.3: file associated to routes -query = QUERY_CHECK_TABLE % 'main_routefile' -cursor.execute(query) -transaction.commit_unless_managed() - -row = cursor.fetchone() -if not row: - query_create = """ - CREATE TABLE "main_routefile" ( - "id" serial NOT NULL PRIMARY KEY, - "name" varchar(150) NOT NULL, - "raw_file" varchar(100) NOT NULL, - "simplified_file" varchar(100), - "file_type" varchar(1) NOT NULL - ) - ; - ALTER TABLE "main_route" ADD COLUMN - "associated_file_id" integer REFERENCES "main_routefile" ("id") - DEFERRABLE INITIALLY DEFERRED; - """ - cursor.execute(query_create) - transaction.commit_unless_managed() - print " * main_routefile created" - -# early versions before 0.1: save route with wrong SRID -# only errors with default SRID is managed adapt the script for your SRID - -changed = False -routes = Route.objects.all() -for route in routes: - # only one test: assume each point as been save with the same SRID... - if route.route and route.route.srid == 4326 and \ - route.route[0][0] > 90 or route.route[0][0] < -90: - changed = True - new_route = [] - for pt in route.route: - pt = proj.TransformPoint(pt[0], pt[1]) - new_route.append((pt[0], pt[1])) - route.route = LineString(new_route) - route.save() -if changed: - print " * projections of routes corrected" diff --git a/chimere/static/chimere/css/styles.css b/chimere/static/chimere/css/styles.css index 9d7ec17..6986189 100644 --- a/chimere/static/chimere/css/styles.css +++ b/chimere/static/chimere/css/styles.css @@ -53,7 +53,7 @@ body, h2, h3, th, fieldset, .action li, #content, #layer_selection #layer_list, -#map-footer, #panel, #chimere_itinerary_panel, #areas, +#map-footer, #panel, #chimere_itinerary_panel, #maps, #welcome, #detail, .detail_footer a, #content .olControlLayerSwitcher .layersDiv, #content .olControlLayerSwitcher span, @@ -76,7 +76,7 @@ div.warning, .errorlist{ #layer_selection h4, #layer_selection #layer_list, -#areas, #detail, #main-map, +#maps, #detail, #main-map, div.warning, #content, .action li.selected, @@ -95,7 +95,7 @@ div.warning, /* entête */ /* -#areas h2, #panel h2{ +#maps h2, #panel h2{ -moz-border-radius: 4px 4px 0 0; -webkit-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; @@ -109,7 +109,7 @@ h2, #action li, .detail_footer{ } fieldset, #content, #panel, -#areas, #welcome, #detail, +#maps, #welcome, #detail, #category_description, div.warning{ -moz-border-radius: 10px; -webkit-border-radius: 10px; @@ -269,18 +269,18 @@ ul#action-2 { width:100%; } -#areas-div, -#areas-div label, +#maps-div, +#maps-div label, #utils-div a{ padding:0.3em; } -#areas{ +#maps{ position:absolute; z-index:5; } -#areas{ +#maps{ bottom:105px; left:18px; padding:0; @@ -289,12 +289,12 @@ ul#action-2 { overflow:auto; } -#areas ul{ +#maps ul{ margin:0; padding:0 10px; } -#areas li{ +#maps li{ list-style:none; } diff --git a/chimere/static/chimere/js/base.js b/chimere/static/chimere/js/base.js index fe8d954..73851a6 100644 --- a/chimere/static/chimere/js/base.js +++ b/chimere/static/chimere/js/base.js @@ -17,8 +17,8 @@ along with this program. If not, see . See the file COPYING for details. */ -var default_area; -var area_name; +var default_map; +var map_name; /* indexOf definition for old IE versions */ if(!Array.indexOf){ @@ -65,7 +65,7 @@ function saveExtent() { /* save the current extent in a cookie */ if(!map) return; var extent_key = 'MAP_EXTENT'; - if (area_name){ extent_key = extent_key + '_' + area_name; } + if (map_name){ extent_key = extent_key + '_' + map_name; } var extent = map.getExtent().transform(map.getProjectionObject(), epsg_display_projection); document.cookie = extent_key + "=" + extent.toArray().join('_') @@ -77,7 +77,7 @@ function getExtent() { var cookies = document.cookie.split(';'); var map_extent; var extent_key = 'MAP_EXTENT'; - if (area_name){ extent_key = extent_key + '_' + area_name; } + if (map_name){ extent_key = extent_key + '_' + map_name; } for (var i=0; i < cookies.length; i++){ var items = cookies[i].split('='); key = items[0].split(' ').join(''); @@ -96,9 +96,9 @@ function zoomToCurrentExtent(map){ extent = new OpenLayers.Bounds(current_extent[0], current_extent[1], current_extent[2], current_extent[3]); } - else if (OpenLayers && default_area && default_area.length == 4){ - extent = new OpenLayers.Bounds(default_area[0], default_area[1], - default_area[2], default_area[3]); + else if (OpenLayers && default_map && default_map.length == 4){ + extent = new OpenLayers.Bounds(default_map[0], default_map[1], + default_map[2], default_map[3]); } else{ return; diff --git a/chimere/static/chimere/js/jquery.chimere-leaflet.js b/chimere/static/chimere/js/jquery.chimere-leaflet.js index c67a7a1..f364bc5 100644 --- a/chimere/static/chimere/js/jquery.chimere-leaflet.js +++ b/chimere/static/chimere/js/jquery.chimere-leaflet.js @@ -47,7 +47,7 @@ See the file COPYING for details. display_submited: false, display_feature: null, display_route: null, - area_id: null, + map_id: null, checked_categories: [], zoom: null, lat: null, @@ -279,7 +279,7 @@ See the file COPYING for details. current_extent = current_extent.replace(/\./g, 'D'); current_extent = current_extent.replace(/-/g, 'M'); var uri = extra_url - if (settings.area_id) uri += settings.area_id + "/"; + if (settings.map_id) uri += settings.map_id + "/"; uri += "getAvailableCategories/"; var params = {"current_extent": current_extent} if (settings.display_submited) params["status"] = "A_S"; diff --git a/chimere/static/chimere/js/jquery.chimere-ol.js b/chimere/static/chimere/js/jquery.chimere-ol.js index 6660b2e..ff07132 100644 --- a/chimere/static/chimere/js/jquery.chimere-ol.js +++ b/chimere/static/chimere/js/jquery.chimere-ol.js @@ -64,7 +64,7 @@ if (typeof(OpenLayers) != 'undefined'){ display_submited: false, display_feature: null, display_route: null, - area_id: null, + map_id: null, checked_categories: [], zoom: null, lat: null, @@ -611,7 +611,7 @@ if (typeof(OpenLayers) != 'undefined'){ current_extent = current_extent.replace(/\./g, 'D'); current_extent = current_extent.replace(/-/g, 'M'); var uri = extra_url - if (settings.area_id) uri += settings.area_id + "/"; + if (settings.map_id) uri += settings.map_id + "/"; uri += "getAvailableCategories/"; var params = {"current_extent": current_extent} if (settings.display_submited) params["status"] = "A_S"; @@ -1277,7 +1277,7 @@ if (typeof(OpenLayers) != 'undefined'){ * update current detail panel with an AJAX request */ var uri = extra_url - if (settings.area_id) uri += settings.area_id + "/" + if (settings.map_id) uri += settings.map_id + "/" uri += "getDetail/" + pk; var params = {} if (settings.simple) { params["simple"] = 1; } diff --git a/chimere/static/leaflet/leaflet-src.js b/chimere/static/leaflet/leaflet-src.js new file mode 100644 index 0000000..13f82bb --- /dev/null +++ b/chimere/static/leaflet/leaflet-src.js @@ -0,0 +1,8338 @@ +/* + Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com + (c) 2010-2013, Vladimir Agafonkin, CloudMade +*/ +(function (window, document, undefined) {/* + * The L namespace contains all Leaflet classes and functions. + * This code allows you to handle any possible namespace conflicts. + */ + +var L, originalL; + +if (typeof exports !== undefined + '') { + L = exports; +} else { + originalL = window.L; + L = {}; + + L.noConflict = function () { + window.L = originalL; + return this; + }; + + window.L = L; +} + +L.version = '0.5.1'; + + +/* + * 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 (/*Object*/ obj) { + obj[key] = obj[key] || ++lastId; + return obj[key]; + }; + }()), + + 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; + }, + + splitWords: function (str) { + return str.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + setOptions: function (obj, options) { + obj.options = L.extend({}, obj.options, options); + return obj.options; + }, + + getParamString: function (obj, existingUrl) { + var params = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + params.push(i + '=' + 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 (!data.hasOwnProperty(key)) { + throw new Error('No value provided for variable ' + str); + } + return value; + }); + }, + + 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; + // 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 key = '_leaflet_events'; + +L.Mixin = {}; + +L.Mixin.Events = { + + addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object]) + var events = this[key] = this[key] || {}, + type, i, len; + + // Types can be a map of types/handlers + if (typeof types === 'object') { + for (type in types) { + if (types.hasOwnProperty(type)) { + this.addEventListener(type, types[type], fn); + } + } + + return this; + } + + types = L.Util.splitWords(types); + + for (i = 0, len = types.length; i < len; i++) { + events[types[i]] = events[types[i]] || []; + events[types[i]].push({ + action: fn, + context: context || this + }); + } + + return this; + }, + + hasEventListeners: function (type) { // (String) -> Boolean + return (key in this) && (type in this[key]) && (this[key][type].length > 0); + }, + + removeEventListener: function (types, fn, context) { // (String[, Function, Object]) or (Object[, Object]) + var events = this[key], + type, i, len, listeners, j; + + if (typeof types === 'object') { + for (type in types) { + if (types.hasOwnProperty(type)) { + this.removeEventListener(type, types[type], fn); + } + } + + return this; + } + + types = L.Util.splitWords(types); + + for (i = 0, len = types.length; i < len; i++) { + + if (this.hasEventListeners(types[i])) { + listeners = events[types[i]]; + + for (j = listeners.length - 1; j >= 0; j--) { + if ( + (!fn || listeners[j].action === fn) && + (!context || (listeners[j].context === context)) + ) { + listeners.splice(j, 1); + } + } + } + } + + return this; + }, + + fireEvent: function (type, data) { // (String[, Object]) + if (!this.hasEventListeners(type)) { + return this; + } + + var event = L.extend({ + type: type, + target: this + }, data); + + var listeners = this[key][type].slice(); + + for (var i = 0, len = listeners.length; i < len; i++) { + listeners[i].action.call(listeners[i].context || this, event); + } + + return this; + } +}; + +L.Mixin.Events.on = L.Mixin.Events.addEventListener; +L.Mixin.Events.off = L.Mixin.Events.removeEventListener; +L.Mixin.Events.fire = L.Mixin.Events.fireEvent; + + +/* + * L.Browser handles different browser and feature detections for internal Leaflet use. + */ + +(function () { + + var ie = !!window.ActiveXObject, + ie6 = ie && !window.XMLHttpRequest, + ie7 = ie && !document.querySelector, + + // 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, + android = ua.indexOf('android') !== -1, + android23 = ua.search('android [23]') !== -1, + + mobile = typeof orientation !== undefined + '', + msTouch = window.navigator && window.navigator.msPointerEnabled && + window.navigator.msMaxTouchPoints, + 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()), + gecko3d = 'MozPerspective' in doc.style, + opera3d = 'OTransition' in doc.style, + any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d); + + + var touch = !window.L_NO_TOUCH && (function () { + + var startName = 'ontouchstart'; + + // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.MsTouch) or WebKit, etc. + if (msTouch || (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, + ie6: ie6, + ie7: ie7, + webkit: webkit, + + 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, + msTouch: msTouch, + + 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) { + return point.x === this.x && + point.y === 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 (isNaN(x)) { + 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, + pos, + ie7 = L.Browser.ie7; + + 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 || 0; + left += docBody.scrollLeft || 0; + break; + } + el = el.offsetParent; + + } while (el); + + el = element; + + do { + if (el === docBody) { break; } + + top -= el.scrollTop || 0; + left -= el.scrollLeft || 0; + + // webkit (and ie <= 7) handles RTL scrollLeft different to everyone else + // https://code.google.com/p/closure-library/source/browse/trunk/closure/goog/style/bidi.js + if (!L.DomUtil.documentIsLtr() && (L.Browser.webkit || ie7)) { + left += el.scrollWidth - el.clientWidth; + + // ie7 shows the scrollbar by default and provides clientWidth counting it, so we + // need to add it back in if it is visible; scrollbar is on the left as we are RTL + if (ie7 && L.DomUtil.getStyle(el, 'overflow-y') !== 'hidden' && + L.DomUtil.getStyle(el, 'overflow') !== 'hidden') { + left += 17; + } + } + + 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; + }, + + disableTextSelection: function () { + if (document.selection && document.selection.empty) { + document.selection.empty(); + } + if (!this._onselectstart) { + this._onselectstart = document.onselectstart || null; + document.onselectstart = L.Util.falseFn; + } + }, + + enableTextSelection: function () { + if (document.onselectstart === L.Util.falseFn) { + document.onselectstart = this._onselectstart; + this._onselectstart = null; + } + }, + + hasClass: function (el, name) { + return (el.className.length > 0) && + new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className); + }, + + addClass: function (el, name) { + if (!L.DomUtil.hasClass(el, name)) { + el.className += (el.className ? ' ' : '') + name; + } + }, + + removeClass: function (el, name) { + + function replaceFn(w, match) { + if (match === name) { return ''; } + return w; + } + + el.className = el.className + .replace(/(\S+)\s*/g, replaceFn) + .replace(/(^\s+|\s+$)/, ''); + }, + + 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) {} + + 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]) + + el._leaflet_pos = point; + + if (!disable3D && L.Browser.any3d) { + el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point); + + // workaround for Android 2/3 stability (https://github.com/CloudMade/Leaflet/issues/69) + if (L.Browser.mobileWebkit3d) { + el.style.WebkitBackfaceVisibility = 'hidden'; + } + } 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 + 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'; + + +/* + * L.LatLng represents a geographical point with latitude and longitude coordinates. + */ + +L.LatLng = function (rawLat, rawLng) { // (Number, Number) + var lat = parseFloat(rawLat), + lng = parseFloat(rawLng); + + if (isNaN(lat) || isNaN(lng)) { + throw new Error('Invalid LatLng object: (' + rawLat + ', ' + rawLng + ')'); + } + + this.lat = lat; + this.lng = lng; +}; + +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)) { + return new L.LatLng(a[0], a[1]); + } + if (isNaN(a)) { + return a; + } + 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 (typeof obj[0] === 'number' || typeof obj[0] === 'string' || obj instanceof L.LatLng) { + obj = L.latLng(obj); + } 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._northEast.lat, this._southWest.lng); + }, + + getSouthEast: function () { + return new L.LatLng(this._southWest.lat, this._northEast.lng); + }, + + 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 () { + var sw = this._southWest, + ne = this._northEast; + + return [sw.lng, sw.lat, ne.lng, ne.lat].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); + } +}; + + +/* + * 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(); + this.callInitHooks(); + this._initEvents(); + + if (options.maxBounds) { + this.setMaxBounds(options.maxBounds); + } + + if (options.center && options.zoom !== undefined) { + this.setView(L.latLng(options.center), options.zoom, true); + } + + this._initLayers(options.layers); + }, + + + // public methods that modify map state + + // replaced by animation-powered implementation in Map.PanAnimation.js + setView: function (center, zoom) { + this._resetView(L.latLng(center), this._limitZoom(zoom)); + return this; + }, + + setZoom: function (zoom) { // (Number) + return this.setView(this.getCenter(), zoom); + }, + + zoomIn: function (delta) { + return this.setZoom(this._zoom + (delta || 1)); + }, + + zoomOut: function (delta) { + return this.setZoom(this._zoom - (delta || 1)); + }, + + fitBounds: function (bounds) { // (LatLngBounds) + var zoom = this.getBoundsZoom(bounds); + return this.setView(L.latLngBounds(bounds).getCenter(), zoom); + }, + + fitWorld: function () { + var sw = new L.LatLng(-60, -170), + ne = new L.LatLng(85, 179); + + return this.fitBounds(new L.LatLngBounds(sw, ne)); + }, + + panTo: function (center) { // (LatLng) + return this.setView(center, this._zoom); + }, + + panBy: function (offset) { // (Point) + // replaced with animated panBy in Map.Animation.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) { + this._boundsMinZoom = null; + return this; + } + + var minZoom = this.getBoundsZoom(bounds, true); + + this._boundsMinZoom = minZoom; + + if (this._loaded) { + if (this._zoom < minZoom) { + this.setView(bounds.getCenter(), minZoom); + } else { + this.panInsideBounds(bounds); + } + } + + return this; + }, + + panInsideBounds: function (bounds) { + bounds = L.latLngBounds(bounds); + + var viewBounds = this.getBounds(), + viewSw = this.project(viewBounds.getSouthWest()), + viewNe = this.project(viewBounds.getNorthEast()), + sw = this.project(bounds.getSouthWest()), + ne = this.project(bounds.getNorthEast()), + dx = 0, + dy = 0; + + if (viewNe.y < ne.y) { // north + dy = ne.y - viewNe.y; + } + if (viewNe.x > ne.x) { // east + dx = ne.x - viewNe.x; + } + if (viewSw.y > sw.y) { // south + dy = sw.y - viewSw.y; + } + if (viewSw.x < sw.x) { // west + dx = sw.x - viewSw.x; + } + + return this.panBy(new L.Point(dx, dy, true)); + }, + + 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); + } + + this.whenReady(function () { + layer.onAdd(this); + this.fire('layeradd', {layer: layer}); + }, this); + + return this; + }, + + removeLayer: function (layer) { + var id = L.stamp(layer); + + if (!this._layers[id]) { return; } + + layer.onRemove(this); + + delete this._layers[id]; + 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.fire('layerremove', {layer: layer}); + }, + + hasLayer: function (layer) { + var id = L.stamp(layer); + return this._layers.hasOwnProperty(id); + }, + + invalidateSize: function (animate) { + var oldSize = this.getSize(); + + this._sizeChanged = true; + + if (this.options.maxBounds) { + this.setMaxBounds(this.options.maxBounds); + } + + if (!this._loaded) { return this; } + + var offset = oldSize._subtract(this.getSize())._divideBy(2)._round(); + + if (animate === true) { + this.panBy(offset); + } else { + this._rawPanBy(offset); + + this.fire('move'); + + clearTimeout(this._sizeTimer); + this._sizeTimer = setTimeout(L.bind(this.fire, this, 'moveend'), 200); + } + return this; + }, + + // TODO handler.addTo + addHandler: function (name, HandlerClass) { + if (!HandlerClass) { return; } + + this[name] = new HandlerClass(this); + + if (this.options[name]) { + this[name].enable(); + } + + return this; + }, + + + // public methods for getting map state + + getCenter: function () { // (Boolean) -> LatLng + 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 () { + var z1 = this.options.minZoom || 0, + z2 = this._layersMinZoom || 0, + z3 = this._boundsMinZoom || 0; + + return Math.max(z1, z2, z3); + }, + + getMaxZoom: function () { + var z1 = this.options.maxZoom === undefined ? Infinity : this.options.maxZoom, + z2 = this._layersMaxZoom === undefined ? Infinity : this._layersMaxZoom; + + return Math.min(z1, z2); + }, + + getBoundsZoom: function (bounds, inside) { // (LatLngBounds, Boolean) -> Number + bounds = L.latLngBounds(bounds); + + var size = this.getSize(), + zoom = this.options.minZoom || 0, + maxZoom = this.getMaxZoom(), + ne = bounds.getNorthEast(), + sw = bounds.getSouthWest(), + boundsSize, + nePoint, + swPoint, + zoomNotFound = true; + + if (inside) { + zoom--; + } + + do { + zoom++; + nePoint = this.project(ne, zoom); + swPoint = this.project(sw, zoom); + + boundsSize = new L.Point( + Math.abs(nePoint.x - swPoint.x), + Math.abs(swPoint.y - nePoint.y)); + + if (!inside) { + zoomNotFound = boundsSize.x <= size.x && boundsSize.y <= size.y; + } else { + zoomNotFound = 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 () { + 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._initialTopLeftPoint); + return this.unproject(projectedPoint); + }, + + latLngToLayerPoint: function (latlng) { // (LatLng) + var projectedPoint = this.project(L.latLng(latlng))._round(); + return projectedPoint._subtract(this._initialTopLeftPoint); + }, + + 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._leaflet) { + throw new Error("Map container is already initialized."); + } + + container._leaflet = true; + }, + + _initLayout: function () { + var container = this._container; + + L.DomUtil.addClass(container, 'leaflet-container'); + + if (L.Browser.touch) { + L.DomUtil.addClass(container, 'leaflet-touch'); + } + + if (this.options.fadeAnimation) { + L.DomUtil.addClass(container, '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); + }, + + _initLayers: function (layers) { + layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; + + this._layers = {}; + this._zoomBoundLayers = {}; + this._tileLayersNum = 0; + + var i, len; + + for (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._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}); + + this.fire('move'); + + if (zoomChanged || afterZoomAnim) { + this.fire('zoomend'); + } + + this.fire('moveend', {hard: !preserveMapOffset}); + + if (loading) { + this.fire('load'); + } + }, + + _rawPanBy: function (offset) { + L.DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset)); + }, + + _updateZoomLevels: function () { + var i, + minZoom = Infinity, + maxZoom = -Infinity; + + for (i in this._zoomBoundLayers) { + if (this._zoomBoundLayers.hasOwnProperty(i)) { + 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; + } + }, + + // map events + + _initEvents: function () { + if (!L.DomEvent) { return; } + + L.DomEvent.on(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.on(this._container, events[i], this._fireMouseEvent, this); + } + + if (this.options.trackResize) { + L.DomEvent.on(window, 'resize', this._onResize, this); + } + }, + + _onResize: function () { + L.Util.cancelAnimFrame(this._resizeRequest); + this._resizeRequest = L.Util.requestAnimFrame( + this.invalidateSize, this, false, this._container); + }, + + _onMouseClick: function (e) { + if (!this._loaded || (this.dragging && this.dragging.moved())) { return; } + + this.fire('preclick'); + this._fireMouseEvent(e); + }, + + _fireMouseEvent: function (e) { + if (!this._loaded) { 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 () { + // TODO super-ugly, refactor!!! + // clear scaled tiles after all new tiles are loaded (for performance) + this._tileLayersToLoad--; + if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) { + clearTimeout(this._clearTileBgTimer); + this._clearTileBgTimer = setTimeout(L.bind(this._clearTileBg, this), 500); + } + }, + + whenReady: function (callback, context) { + if (this._loaded) { + callback.call(context || this, this); + } else { + this.on('load', callback, context); + } + return this; + }, + + + // private methods for getting map state + + _getMapPanePos: function () { + return L.DomUtil.getPosition(this._mapPane); + }, + + _getTopLeftPoint: function () { + if (!this._loaded) { + throw new Error('Set map center and zoom first.'); + } + + return this._initialTopLeftPoint.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); + }, + + _getCenterLayerPoint: function () { + return this.containerPointToLayerPoint(this.getSize()._divideBy(2)); + }, + + _getCenterOffset: function (center) { + return this.latLngToLayerPoint(center).subtract(this._getCenterLayerPoint()); + }, + + _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.3142, + 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 = -r2 * 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 / r2), + 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, + r2 = m.R_MINOR; + + return new L.Transformation(0.5 / (Math.PI * r), 0.5, -0.5 / (Math.PI * r2), 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, + /* (undefined works too) + zIndex: null, + tms: false, + continuousWorld: false, + noWrap: false, + zoomReverse: false, + detectRetina: false, + reuseTiles: 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--; + } + + this._url = url; + + var subdomains = this.options.subdomains; + + if (typeof subdomains === 'string') { + this.options.subdomains = subdomains.split(''); + } + }, + + onAdd: function (map) { + this._map = map; + + // create a container div for tiles + this._initContainer(); + + // create an image to clone for tiles + this._createTileProto(); + + // set up events + map.on({ + 'viewreset': this._resetCallback, + 'moveend': this._update + }, 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._resetCallback, + 'moveend': this._update + }, 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; + }, + + 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._map._panes.tilePane.empty = false; + this._reset(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 () { + L.DomUtil.setOpacity(this._container, this.options.opacity); + + // stupid webkit hack to force redrawing of tiles + var i, + tiles = this._tiles; + + if (L.Browser.webkit) { + for (i in tiles) { + if (tiles.hasOwnProperty(i)) { + tiles[i].style.webkitTransform += ' translate(0,0)'; + } + } + } + }, + + _initContainer: function () { + var tilePane = this._map._panes.tilePane; + + if (!this._container || tilePane.empty) { + this._container = L.DomUtil.create('div', 'leaflet-layer'); + + this._updateZIndex(); + + tilePane.appendChild(this._container); + + if (this.options.opacity < 1) { + this._updateOpacity(); + } + } + }, + + _resetCallback: function (e) { + this._reset(e.hard); + }, + + _reset: function (clearOldContainer) { + var tiles = this._tiles; + + for (var key in tiles) { + if (tiles.hasOwnProperty(key)) { + this.fire('tileunload', {tile: tiles[key]}); + } + } + + this._tiles = {}; + this._tilesToLoad = 0; + + if (this.options.reuseTiles) { + this._unusedTiles = []; + } + + if (clearOldContainer && this._container) { + this._container.innerHTML = ""; + } + + this._initContainer(); + }, + + _update: function () { + + if (!this._map) { return; } + + var bounds = this._map.getPixelBounds(), + zoom = this._map.getZoom(), + tileSize = this.options.tileSize; + + if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { + return; + } + + var nwTilePoint = new L.Point( + Math.floor(bounds.min.x / tileSize), + Math.floor(bounds.min.y / tileSize)), + + seTilePoint = new L.Point( + Math.floor(bounds.max.x / tileSize), + Math.floor(bounds.max.y / tileSize)), + + tileBounds = new L.Bounds(nwTilePoint, seTilePoint); + + 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._container.appendChild(fragment); + }, + + _tileShouldBeLoaded: function (tilePoint) { + if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) { + return false; // already loaded + } + + if (!this.options.continuousWorld) { + var limit = this._getWrapTileNum(); + + if (this.options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit) || + tilePoint.y < 0 || tilePoint.y >= limit) { + return false; // exceeds world bounds + } + } + + return true; + }, + + _removeOtherTiles: function (bounds) { + var kArr, x, y, key; + + for (key in this._tiles) { + if (this._tiles.hasOwnProperty(key)) { + 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._container) { + this._container.removeChild(tile); + } + + // for https://github.com/CloudMade/Leaflet/issues/137 + if (!L.Browser.android) { + 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 + Android 3 browser not tested + Android 2 browser requires top/left or tiles disappear on load or first drag + (reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866 + (other browsers don't currently care) - see debug/hacks/jitter.html for an example + */ + L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23); + + this._tiles[tilePoint.x + ':' + tilePoint.y] = tile; + + this._loadTile(tile, tilePoint); + + if (tile.parentNode !== this._container) { + container.appendChild(tile); + } + }, + + _getZoomForUrl: function () { + + var options = this.options, + zoom = this._map.getZoom(); + + if (options.zoomReverse) { + zoom = options.maxZoom - zoom; + } + + return zoom + options.zoomOffset; + }, + + _getTilePos: function (tilePoint) { + var origin = this._map.getPixelOrigin(), + tileSize = this.options.tileSize; + + return tilePoint.multiplyBy(tileSize).subtract(origin); + }, + + // image-specific code (override to implement e.g. Canvas or SVG tile layer) + + getTileUrl: function (tilePoint) { + this._adjustTilePoint(tilePoint); + + return L.Util.template(this._url, L.extend({ + s: this._getSubdomain(tilePoint), + z: this._getZoomForUrl(), + x: tilePoint.x, + y: tilePoint.y + }, this.options)); + }, + + _getWrapTileNum: function () { + // TODO refactor, limit is not valid for non-standard projections + return Math.pow(2, this._getZoomForUrl()); + }, + + _adjustTilePoint: function (tilePoint) { + + var limit = this._getWrapTileNum(); + + // wrap tile coordinates + if (!this.options.continuousWorld && !this.options.noWrap) { + tilePoint.x = ((tilePoint.x % limit) + limit) % limit; + } + + if (this.options.tms) { + tilePoint.y = limit - tilePoint.y - 1; + } + }, + + _getSubdomain: function (tilePoint) { + var index = (tilePoint.x + tilePoint.y) % this.options.subdomains.length; + return this.options.subdomains[index]; + }, + + _createTileProto: function () { + var img = this._tileImg = L.DomUtil.create('img', 'leaflet-tile'); + img.style.width = img.style.height = this.options.tileSize + 'px'; + img.galleryimg = 'no'; + }, + + _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 = this._tileImg.cloneNode(false); + tile.onselectstart = tile.onmousemove = L.Util.falseFn; + return tile; + }, + + _loadTile: function (tile, tilePoint) { + tile._layer = this; + tile.onload = this._tileOnLoad; + tile.onerror = this._tileOnError; + + tile.src = this.getTileUrl(tilePoint); + }, + + _tileLoaded: function () { + this._tilesToLoad--; + if (!this._tilesToLoad) { + this.fire('load'); + } + }, + + _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); + + if (options.detectRetina && L.Browser.retina) { + wmsParams.width = wmsParams.height = this.options.tileSize * 2; + } else { + wmsParams.width = wmsParams.height = this.options.tileSize; + } + + for (var i in options) { + // all keys that are not TileLayer options go to WMS params + if (!this.options.hasOwnProperty(i)) { + wmsParams[i] = options[i]; + } + } + + this.wmsParams = wmsParams; + + L.setOptions(this, options); + }, + + onAdd: function (map) { + + var projectionKey = parseFloat(this.wmsParams.version) >= 1.3 ? 'crs' : 'srs'; + this.wmsParams[projectionKey] = map.options.crs.code; + + L.TileLayer.prototype.onAdd.call(this, map); + }, + + getTileUrl: function (tilePoint, zoom) { // (Point, Number) -> String + + this._adjustTilePoint(tilePoint); + + var map = this._map, + crs = map.options.crs, + tileSize = this.options.tileSize, + + nwPoint = tilePoint.multiplyBy(tileSize), + sePoint = nwPoint.add(new L.Point(tileSize, tileSize)), + + nw = crs.project(map.unproject(nwPoint, zoom)), + se = crs.project(map.unproject(sePoint, zoom)), + + bbox = [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) + "&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 () { + var tiles = this._tiles; + + for (var i in tiles) { + if (tiles.hasOwnProperty(i)) { + this._redrawTile(tiles[i]); + } + } + }, + + _redrawTile: function (tile) { + this.drawTile(tile, tile._tilePoint, this._map._zoom); + }, + + _createTileProto: function () { + var proto = this._canvasProto = L.DomUtil.create('canvas', 'leaflet-tile'); + proto.width = proto.height = this.options.tileSize; + }, + + _createTile: function () { + var tile = this._canvasProto.cloneNode(false); + 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; + }, + + _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: (Point) (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 () { + return this._createIcon('icon'); + }, + + createShadow: function () { + return this._createIcon('shadow'); + }, + + _createIcon: function (name) { + 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 = this._createImg(src); + 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) { + var el; + + if (!L.Browser.ie6) { + el = document.createElement('img'); + el.src = src; + } else { + el = document.createElement('div'); + el.style.filter = + 'progid:DXImageTransform.Microsoft.AlphaImageLoader(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: new L.Point(25, 41), + iconAnchor: new L.Point(12, 41), + popupAnchor: new L.Point(1, -34), + + shadowSize: new L.Point(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; + + for (i = 0, len = scripts.length; i < len; i++) { + src = scripts[i].src; + matches = src.match(leafletRe); + + if (matches) { + return src.split(leafletRe)[0] + '/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: '', + clickable: true, + draggable: false, + 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(); + + if (map.options.zoomAnimation && map.options.markerZoomAnimation) { + map.on('zoomanim', this._animateZoom, this); + } + }, + + addTo: function (map) { + map.addLayer(this); + return this; + }, + + onRemove: function (map) { + this._removeIcon(); + + 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) { + if (this._map) { + this._removeIcon(); + } + + this.options.icon = icon; + + if (this._map) { + this._initIcon(); + this.update(); + } + + 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', + needOpacityUpdate = false; + + if (!this._icon) { + this._icon = options.icon.createIcon(); + + if (options.title) { + this._icon.title = options.title; + } + + this._initInteraction(); + needOpacityUpdate = (this.options.opacity < 1); + + L.DomUtil.addClass(this._icon, classToAdd); + + if (options.riseOnHover) { + L.DomEvent + .on(this._icon, 'mouseover', this._bringToFront, this) + .on(this._icon, 'mouseout', this._resetZIndex, this); + } + } + + if (!this._shadow) { + this._shadow = options.icon.createShadow(); + + if (this._shadow) { + L.DomUtil.addClass(this._shadow, classToAdd); + needOpacityUpdate = (this.options.opacity < 1); + } + } + + if (needOpacityUpdate) { + this._updateOpacity(); + } + + var panes = this._map._panes; + + panes.markerPane.appendChild(this._icon); + + if (this._shadow) { + panes.shadowPane.appendChild(this._shadow); + } + }, + + _removeIcon: function () { + var panes = this._map._panes; + + if (this.options.riseOnHover) { + L.DomEvent + .off(this._icon, 'mouseover', this._bringToFront) + .off(this._icon, 'mouseout', this._resetZIndex); + } + + panes.markerPane.removeChild(this._icon); + + if (this._shadow) { + panes.shadowPane.removeChild(this._shadow); + } + + this._icon = 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); + + 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); + + 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 + }); + }, + + _fireMouseEvent: function (e) { + + this.fire(e.type, { + originalEvent: e + }); + + // 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); + } + }, + + setOpacity: function (opacity) { + this.options.opacity = opacity; + if (this._map) { + this._updateOpacity(); + } + }, + + _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: new L.Point(12, 12), // also can be set through CSS + /* + iconAnchor: (Point) + popupAnchor: (Point) + html: (String) + bgPos: (Point) + */ + className: 'leaflet-div-icon' + }, + + createIcon: function () { + var div = document.createElement('div'), + options = this.options; + + if (options.html) { + div.innerHTML = options.html; + } + + 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: new L.Point(0, 6), + autoPanPadding: new L.Point(5, 5), + className: '', + zoomAnimation: true + }, + + initialize: function (options, source) { + L.setOptions(this, options); + + this._source = source; + this._animated = L.Browser.any3d && this.options.zoomAnimation; + }, + + onAdd: function (map) { + this._map = map; + + if (!this._container) { + this._initLayout(); + } + this._updateContent(); + + var animFade = map.options.fadeAnimation; + + if (animFade) { + L.DomUtil.setOpacity(this._container, 0); + } + map._panes.popupPane.appendChild(this._container); + + map.on('viewreset', this._updatePosition, this); + + if (this._animated) { + map.on('zoomanim', this._zoomAnimation, this); + } + + if (map.options.closePopupOnClick) { + map.on('preclick', this._close, this); + } + + this._update(); + + if (animFade) { + L.DomUtil.setOpacity(this._container, 1); + } + }, + + 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({ + viewreset: this._updatePosition, + preclick: this._close, + zoomanim: this._zoomAnimation + }, this); + + if (map.options.fadeAnimation) { + L.DomUtil.setOpacity(this._container, 0); + } + + this._map = null; + }, + + setLatLng: function (latlng) { + this._latlng = L.latLng(latlng); + this._update(); + return this; + }, + + setContent: function (content) { + this._content = content; + this._update(); + return this; + }, + + _close: function () { + var map = this._map; + + if (map) { + map._popup = null; + + map + .removeLayer(this) + .fire('popupclose', {popup: 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 = '×'; + + 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.on(this._contentNode, 'mousewheel', L.DomEvent.stopPropagation); + + this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container); + this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); + }, + + _update: function () { + if (!this._map) { return; } + + this._container.style.visibility = 'hidden'; + + this._updateContent(); + this._updateLayout(); + this._updatePosition(); + + this._container.style.visibility = ''; + + this._adjustPan(); + }, + + _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 = 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 = this.options.autoPanPadding, + size = map.getSize(), + dx = 0, + dy = 0; + + if (containerPos.x < 0) { + dx = containerPos.x - padding.x; + } + if (containerPos.x + containerWidth > size.x) { + dx = containerPos.x + containerWidth - size.x + padding.x; + } + if (containerPos.y < 0) { + dy = containerPos.y - padding.y; + } + if (containerPos.y + containerHeight > size.y) { + dy = containerPos.y + containerHeight - size.y + padding.y; + } + + if (dx || dy) { + map.panBy(new L.Point(dx, dy)); + } + }, + + _onCloseButtonClick: function (e) { + this._close(); + L.DomEvent.stop(e); + } +}); + +L.popup = function (options, source) { + return new L.Popup(options, source); +}; + + +/* + * Popup extension to L.Marker, adding popup-related methods. + */ + +L.Marker.include({ + openPopup: function () { + if (this._popup && this._map) { + this._popup.setLatLng(this._latlng); + this._map.openPopup(this._popup); + } + + return this; + }, + + closePopup: function () { + if (this._popup) { + this._popup._close(); + } + return this; + }, + + bindPopup: function (content, options) { + var anchor = L.point(this.options.icon.options.popupAnchor) || new L.Point(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._popup) { + this + .on('click', this.openPopup, this) + .on('remove', this.closePopup, this) + .on('move', this._movePopup, this); + } + + this._popup = new L.Popup(options, this) + .setContent(content); + + return this; + }, + + unbindPopup: function () { + if (this._popup) { + this._popup = null; + this + .off('click', this.openPopup) + .off('remove', this.closePopup) + .off('move', this._movePopup); + } + return this; + }, + + _movePopup: function (e) { + this._popup.setLatLng(e.latlng); + } +}); + + +/* + * Adds popup-related methods to L.Map. + */ + +L.Map.include({ + openPopup: function (popup) { + this.closePopup(); + + this._popup = popup; + + return this + .addLayer(popup) + .fire('popupopen', {popup: this._popup}); + }, + + closePopup: function () { + if (this._popup) { + this._popup._close(); + } + return this; + } +}); + + +/* + * 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 = L.stamp(layer); + + this._layers[id] = layer; + + if (this._map) { + this._map.addLayer(layer); + } + + return this; + }, + + removeLayer: function (layer) { + var id = L.stamp(layer); + + delete this._layers[id]; + + if (this._map) { + this._map.removeLayer(layer); + } + + return this; + }, + + 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) { + if (this._layers.hasOwnProperty(i)) { + 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) { + if (this._layers.hasOwnProperty(i)) { + method.call(context, this._layers[i]); + } + } + }, + + setZIndex: function (zIndex) { + return this.invoke('setZIndex', zIndex); + } +}); + +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' + }, + + addLayer: function (layer) { + if (this._layers[L.stamp(layer)]) { + return this; + } + + 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) { + 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); + }, + + 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.layer = e.target; + e.target = this; + + 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: L.Browser.mobile ? + Math.max(0, Math.min(0.5, + (1280 / Math.max(window.innerWidth, window.innerHeight) - 1) / 2)) : 0.5 + }, + + options: { + stroke: true, + color: '#0033ff', + dashArray: 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'); + 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'); + } + 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'); + } + } 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) { + this._path.setAttribute('class', '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) { + this._pathRoot.setAttribute('class', ' leaflet-zoom-animated'); + + this.on({ + 'zoomanim': this._animatePathZoom, + 'zoomend': this._endPathZoom + }); + } else { + this._pathRoot.setAttribute('class', ' 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 (!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 = ''; + + var shape = div.firstChild; + shape.style.behavior = 'url(#default#VML)'; + + return shape && (typeof shape.adj === 'object'); + + } catch (e) { + return false; + } +}()); + +L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({ + statics: { + VML: true, + CLIP_PADDING: 0.02 + }, + + _createElement: (function () { + try { + document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); + return function (name) { + return document.createElement(''); + }; + } catch (e) { + return function (name) { + return document.createElement( + '<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); + }; + } + }()), + + _initPath: function () { + var container = this._container = this._createElement('shape'); + L.DomUtil.addClass(container, 'leaflet-vml-shape'); + 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 = options.dashArray instanceof Array ? + options.dashArray.join(' ') : + options.dashArray.replace(/ *, */g, ' '); + } else { + stroke.dashStyle = ''; + } + + } 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._requestUpdate(); + + 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 hand cursor + // TODO mouseover, mouseout, dblclick + this._map.on('click', this._onClick, this); + } + }, + + _onClick: function (e) { + if (this._containsPoint(e.layerPoint)) { + this.fire('click', { + latlng: e.latlng, + layerPoint: e.layerPoint, + containerPoint: e.containerPoint, + originalEvent: 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 oprations 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.Polygon 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); + 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 () { + var bounds = new L.LatLngBounds(), + latLngs = this.getLatLngs(), + i, len; + + for (i = 0, len = latLngs.length; i < len; i++) { + bounds.extend(latLngs[i]); + } + + return bounds; + }, + + _convertLatLngs: function (latlngs) { + var i, len; + for (i = 0, len = latlngs.length; i < len; i++) { + if (L.Util.isArray(latlngs[i]) && typeof latlngs[i][0] !== 'number') { + return; + } + latlngs[i] = L.latLng(latlngs[i]); + } + return latlngs; + }, + + _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); + + if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { + this._latlngs = this._convertLatLngs(latlngs[0]); + this._holes = latlngs.slice(1); + } + }, + + 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]); + } + } + }, + + _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; + } + }); + } + + 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(), + latlng2 = new L.LatLng(this._latlng.lat, this._latlng.lng - lngRadius), + point2 = this._map.latLngToLayerPoint(latlng2); + + this._point = this._map.latLngToLayerPoint(this._latlng); + this._radius = Math.max(Math.round(this._point.x - point2.x), 1); + }, + + getBounds: function () { + var lngRadius = this._getLngRadius(), + latRadius = (this._mRadius / 40075017) * 360, + latlng = this._latlng, + sw = new L.LatLng(latlng.lat - latRadius, latlng.lng - lngRadius), + ne = new L.LatLng(latlng.lat + latRadius, latlng.lng + lngRadius); + + return new L.LatLngBounds(sw, ne); + }, + + 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); + }, + + setRadius: function (radius) { + this.options.radius = this._radius = radius; + return this.redraw(); + } +}); + +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); + } +}); + + +/* + * 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; + + if (features) { + for (i = 0, len = features.length; i < len; i++) { + // Only add this if geometry or geometries are set and not null + if (features[i].geometries || features[i].geometry || features[i].features) { + 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); + layer.feature = 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) { + var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, + coords = geometry.coordinates, + layers = [], + latlng, latlngs, i, len, layer; + + switch (geometry.type) { + case 'Point': + latlng = this.coordsToLatLng(coords); + return pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng); + + case 'MultiPoint': + for (i = 0, len = coords.length; i < len; i++) { + latlng = this.coordsToLatLng(coords[i]); + layer = pointToLayer ? pointToLayer(geojson, latlng) : new L.Marker(latlng); + layers.push(layer); + } + return new L.FeatureGroup(layers); + + case 'LineString': + latlngs = this.coordsToLatLngs(coords); + return new L.Polyline(latlngs); + + case 'Polygon': + latlngs = this.coordsToLatLngs(coords, 1); + return new L.Polygon(latlngs); + + case 'MultiLineString': + latlngs = this.coordsToLatLngs(coords, 1); + return new L.MultiPolyline(latlngs); + + case 'MultiPolygon': + latlngs = this.coordsToLatLngs(coords, 2); + return new L.MultiPolygon(latlngs); + + case 'GeometryCollection': + for (i = 0, len = geometry.geometries.length; i < len; i++) { + layer = this.geometryToLayer({ + geometry: geometry.geometries[i], + type: 'Feature', + properties: geojson.properties + }, pointToLayer); + layers.push(layer); + } + return new L.FeatureGroup(layers); + + default: + throw new Error('Invalid GeoJSON object.'); + } + }, + + coordsToLatLng: function (coords, reverse) { // (Array, Boolean) -> LatLng + var lat = parseFloat(coords[reverse ? 0 : 1]), + lng = parseFloat(coords[reverse ? 1 : 0]); + + return new L.LatLng(lat, lng); + }, + + coordsToLatLngs: function (coords, levelsDeep, reverse) { // (Array, Number, Boolean) -> Array + var latlng, + latlngs = [], + i, len; + + for (i = 0, len = coords.length; i < len; i++) { + latlng = levelsDeep ? + this.coordsToLatLngs(coords[i], levelsDeep - 1, reverse) : + this.coordsToLatLng(coords[i], reverse); + + latlngs.push(latlng); + } + + return latlngs; + } +}); + +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.msTouch && type.indexOf('touch') === 0) { + return this.addMsTouchListener(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 { + 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; } + + if (L.Browser.msTouch && type.indexOf('touch') === 0) { + this.removeMsTouchListener(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; + } + return this; + }, + + disableClickPropagation: function (el) { + + var stop = L.DomEvent.stopPropagation; + + for (var i = L.Draggable.START.length - 1; i >= 0; i--) { + L.DomEvent.addListener(el, L.Draggable.START[i], stop); + } + + return L.DomEvent + .addListener(el, 'click', stop) + .addListener(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) { + + var body = document.body, + docEl = document.documentElement, + x = e.pageX ? e.pageX : e.clientX + body.scrollLeft + docEl.scrollLeft, + y = e.pageY ? e.pageY : e.clientY + body.scrollTop + docEl.scrollTop, + pos = new L.Point(x, y); + + return (container ? pos._subtract(L.DomUtil.getViewportOffset(container)) : pos); + }, + + getWheelDelta: function (e) { + + var delta = 0; + + if (e.wheelDelta) { + delta = e.wheelDelta / 120; + } + if (e.detail) { + delta = -e.detail / 3; + } + return delta; + }, + + // 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; + } +}; + +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', + MSPointerDown: 'touchend' + }, + MOVE: { + mousedown: 'mousemove', + touchstart: 'touchmove', + MSPointerDown: 'touchmove' + }, + TAP_TOLERANCE: 15 + }, + + initialize: function (element, dragStartTarget, longPress) { + this._element = element; + this._dragStartTarget = dragStartTarget || element; + this._longPress = longPress && !L.Browser.msTouch; + }, + + 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) { + if ((!L.Browser.touch && e.shiftKey) || + ((e.which !== 1) && (e.button !== 1) && !e.touches)) { return; } + + L.DomEvent.preventDefault(e); + L.DomEvent.stopPropagation(e); + + if (L.Draggable._disabled) { return; } + + this._simulateClick = true; + + if (e.touches && e.touches.length > 1) { + this._simulateClick = false; + clearTimeout(this._longPressTimeout); + return; + } + + var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), + el = first.target; + + if (L.Browser.touch && el.tagName.toLowerCase() === 'a') { + L.DomUtil.addClass(el, 'leaflet-active'); + } + + this._moved = false; + if (this._moving) { return; } + + this._startPoint = new L.Point(first.clientX, first.clientY); + this._startPos = this._newPos = L.DomUtil.getPosition(this._element); + + //Touch contextmenu event emulation + if (e.touches && e.touches.length === 1 && L.Browser.touch && this._longPress) { + this._longPressTimeout = setTimeout(L.bind(function () { + var dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0; + + if (dist < L.Draggable.TAP_TOLERANCE) { + this._simulateClick = false; + this._onUp(); + this._simulateEvent('contextmenu', first); + } + }, this), 1000); + } + + L.DomEvent.on(document, L.Draggable.MOVE[e.type], this._onMove, this); + L.DomEvent.on(document, L.Draggable.END[e.type], this._onUp, this); + }, + + _onMove: function (e) { + if (e.touches && e.touches.length > 1) { return; } + + var first = (e.touches && e.touches.length === 1 ? e.touches[0] : e), + newPoint = new L.Point(first.clientX, first.clientY), + diffVec = newPoint.subtract(this._startPoint); + + if (!diffVec.x && !diffVec.y) { return; } + + L.DomEvent.preventDefault(e); + + if (!this._moved) { + this.fire('dragstart'); + this._moved = true; + + this._startPos = L.DomUtil.getPosition(this._element).subtract(diffVec); + + if (!L.Browser.touch) { + L.DomUtil.disableTextSelection(); + this._setMovingCursor(); + } + } + + this._newPos = this._startPos.add(diffVec); + 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 (e) { + var simulateClickTouch; + clearTimeout(this._longPressTimeout); + if (this._simulateClick && e.changedTouches) { + var first = e.changedTouches[0], + el = first.target, + dist = (this._newPos && this._newPos.distanceTo(this._startPos)) || 0; + + if (el.tagName.toLowerCase() === 'a') { + L.DomUtil.removeClass(el, 'leaflet-active'); + } + + if (dist < L.Draggable.TAP_TOLERANCE) { + simulateClickTouch = first; + } + } + + if (!L.Browser.touch) { + L.DomUtil.enableTextSelection(); + this._restoreCursor(); + } + + for (var i in L.Draggable.MOVE) { + if (L.Draggable.MOVE.hasOwnProperty(i)) { + L.DomEvent.off(document, L.Draggable.MOVE[i], this._onMove); + L.DomEvent.off(document, L.Draggable.END[i], this._onUp); + } + } + + if (this._moved) { + // ensure drag is not fired after dragend + L.Util.cancelAnimFrame(this._animRequest); + + this.fire('dragend'); + } + this._moving = false; + + if (simulateClickTouch) { + this._moved = false; + this._simulateEvent('click', simulateClickTouch); + } + }, + + _setMovingCursor: function () { + L.DomUtil.addClass(document.body, 'leaflet-dragging'); + }, + + _restoreCursor: function () { + L.DomUtil.removeClass(document.body, 'leaflet-dragging'); + }, + + _simulateEvent: function (type, e) { + var simulatedEvent = document.createEvent('MouseEvents'); + + 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); + } +}); + + +/* + 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, + + longPress: true, + + // 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, map.options.longPress); + + 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); + } + } + 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(new L.LatLng(0, 0)); + + this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x; + this._worldWidth = this._map.project(new L.LatLng(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 () { + var map = this._map, + options = map.options, + delay = +new Date() - this._lastTime, + + noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0]; + + 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(new L.Point(0, 0)), + + limitedSpeed = Math.min(options.inertiaMaxSpeed, speed), + limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed), + + decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease), + offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round(); + + L.Util.requestAnimFrame(function () { + map.panBy(offset, decelerationDuration, ease); + }); + } + + map.fire('dragend'); + + if (options.maxBounds) { + // TODO predrag validation instead of animation + L.Util.requestAnimFrame(this._panInsideMaxBounds, map, true, map._container); + } + }, + + _panInsideMaxBounds: function () { + this.panInsideBounds(this.options.maxBounds); + } +}); + +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); + }, + + removeHooks: function () { + this._map.off('dblclick', this._onDoubleClick); + }, + + _onDoubleClick: function (e) { + this.setView(e.latlng, this._zoom + 1); + } +}); + +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); + this._delta = 0; + }, + + removeHooks: function () { + L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll); + }, + + _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.round(delta); + delta = Math.max(Math.min(delta, 4), -4); + delta = map._limitZoom(zoom + delta) - zoom; + + this._delta = 0; + + this._startTime = null; + + if (!delta) { return; } + + var newZoom = zoom + delta, + newCenter = this._getCenterForScrollWheelZoom(newZoom); + + map.setView(newCenter, newZoom); + }, + + _getCenterForScrollWheelZoom: function (newZoom) { + var map = this._map, + scale = map.getZoomScale(newZoom), + viewHalf = map.getSize()._divideBy(2), + centerOffset = this._lastMousePos._subtract(viewHalf)._multiplyBy(1 - 1 / scale), + newCenterPoint = map._getTopLeftPoint()._add(viewHalf)._add(centerOffset); + + return map.unproject(newCenterPoint); + } +}); + +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.msTouch ? 'MSPointerDown' : 'touchstart', + _touchend: L.Browser.msTouch ? 'MSPointerUp' : '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.msTouch) { + 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) { + /*jshint forin:false */ + if (L.Browser.msTouch) { + var idx = trackedTouches.indexOf(e.pointerId); + if (idx === -1) { + return; + } + trackedTouches.splice(idx, 1); + } + + if (doubleTap) { + if (L.Browser.msTouch) { + //Work around .type being readonly with MSPointer* events + var newTouch = { }, + prop; + + 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 msTouch 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.msTouch ? document.documentElement : obj; + + obj.addEventListener(touchstart, onTouchStart, false); + endElement.addEventListener(touchend, onTouchEnd, false); + if (L.Browser.msTouch) { + endElement.addEventListener('MSPointerCancel', onTouchEnd, false); + } + return this; + }, + + removeDoubleTapListener: function (obj, id) { + var pre = '_leaflet_'; + obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); + (L.Browser.msTouch ? document.documentElement : obj).removeEventListener(this._touchend, obj[pre + this._touchend + id], false); + if (L.Browser.msTouch) { + document.documentElement.removeEventListener('MSPointerCancel', 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, { + + _msTouches: [], + _msDocumentListener: false, + + // Provides a touch events wrapper for msPointer events. + // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019 + + addMsTouchListener: function (obj, type, handler, id) { + + switch (type) { + case 'touchstart': + return this.addMsTouchListenerStart(obj, type, handler, id); + case 'touchend': + return this.addMsTouchListenerEnd(obj, type, handler, id); + case 'touchmove': + return this.addMsTouchListenerMove(obj, type, handler, id); + default: + throw 'Unknown touch event type'; + } + }, + + addMsTouchListenerStart: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._msTouches; + + var cb = function (e) { + + var alreadyInArray = false; + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + alreadyInArray = true; + break; + } + } + if (!alreadyInArray) { + touches.push(e); + } + + e.touches = touches.slice(); + e.changedTouches = [e]; + + handler(e); + }; + + obj[pre + 'touchstart' + id] = cb; + obj.addEventListener('MSPointerDown', cb, false); + + // need to also listen for end events to keep the _msTouches list accurate + // this needs to be on the body and never go away + if (!this._msDocumentListener) { + var internalCb = function (e) { + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + touches.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('MSPointerUp', internalCb, false); + document.documentElement.addEventListener('MSPointerCancel', internalCb, false); + + this._msDocumentListener = true; + } + + return this; + }, + + addMsTouchListenerMove: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._msTouches; + + function cb(e) { + + // don't fire touch moves when mouse isn't down + if (e.pointerType === e.MSPOINTER_TYPE_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('MSPointerMove', cb, false); + + return this; + }, + + addMsTouchListenerEnd: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._msTouches; + + 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('MSPointerUp', cb, false); + obj.addEventListener('MSPointerCancel', cb, false); + + return this; + }, + + removeMsTouchListener: function (obj, type, id) { + var pre = '_leaflet_', + cb = obj[pre + type + id]; + + switch (type) { + case 'touchstart': + obj.removeEventListener('MSPointerDown', cb, false); + break; + case 'touchmove': + obj.removeEventListener('MSPointerMove', cb, false); + break; + case 'touchend': + obj.removeEventListener('MSPointerUp', cb, false); + obj.removeEventListener('MSPointerCancel', 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 +}); + +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) { + if (!e.touches || e.touches.length !== 2) { return; } + + var map = this._map; + + 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 (!this._moved) { + L.DomUtil.addClass(map._mapPane, 'leaflet-zoom-anim leaflet-touching'); + + map + .fire('movestart') + .fire('zoomstart') + ._prepareTileBg(); + + 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); + + map.fire('zoomanim', { + center: center, + zoom: map.getScaleZoom(this._scale) + }); + + // Used 2 translates instead of transform-origin because of a very strange bug - + // it didn't count the origin on the first touch-zoom but worked correctly afterwards + + map._tileBg.style[L.DomUtil.TRANSFORM] = + L.DomUtil.getTranslateString(this._delta) + ' ' + + L.DomUtil.getScaleString(this._scale, this._startCenter); + }, + + _onTouchEnd: function () { + if (!this._moved || !this._zooming) { return; } + + var map = this._map; + + this._zooming = false; + L.DomUtil.removeClass(map._mapPane, 'leaflet-touching'); + + 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); + + map.fire('zoomanim', { + center: center, + zoom: zoom + }); + + map._runAnimation(center, zoom, map.getZoomScale(zoom) / this._scale, origin, true); + }, + + _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.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; + }, + + addHooks: function () { + L.DomEvent.on(this._container, 'mousedown', this._onMouseDown, this); + }, + + removeHooks: function () { + L.DomEvent.off(this._container, 'mousedown', this._onMouseDown); + }, + + _onMouseDown: function (e) { + if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } + + L.DomUtil.disableTextSelection(); + + this._startLayerPoint = this._map.mouseEventToLayerPoint(e); + + 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'; + + L.DomEvent + .on(document, 'mousemove', this._onMouseMove, this) + .on(document, 'mouseup', this._onMouseUp, this) + .preventDefault(e); + + this._map.fire("boxzoomstart"); + }, + + _onMouseMove: function (e) { + 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); + + // 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'; + }, + + _onMouseUp: function (e) { + this._pane.removeChild(this._box); + this._container.style.cursor = ''; + + L.DomUtil.enableTextSelection(); + + L.DomEvent + .off(document, 'mousemove', this._onMouseMove) + .off(document, 'mouseup', this._onMouseUp); + + 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 + }); + } +}); + +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], + 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) { + this._map._container.focus(); + } + }, + + _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 (this._panKeys.hasOwnProperty(key)) { + map.panBy(this._panKeys[key]); + + if (map.options.maxBounds) { + map.panInsideBounds(map.options.maxBounds); + } + + } else if (this._zoomKeys.hasOwnProperty(key)) { + 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) + .on('dragstart', this._onDragStart, this) + .on('drag', this._onDrag, this) + .on('dragend', this._onDragEnd, this); + } + this._draggable.enable(); + }, + + removeHooks: function () { + this._draggable.disable(); + }, + + 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 () { + this._marker + .fire('moveend') + .fire('dragend'); + } +}); + + +/* + * L.Handler.PolyEdit is an editing handler for polylines and polygons. + */ + +L.Handler.PolyEdit = L.Handler.extend({ + options: { + icon: new L.DivIcon({ + iconSize: new L.Point(8, 8), + className: 'leaflet-div-icon leaflet-editing-icon' + }) + }, + + initialize: function (poly, options) { + this._poly = poly; + L.setOptions(this, options); + }, + + addHooks: function () { + if (this._poly._map) { + if (!this._markerGroup) { + this._initMarkers(); + } + this._poly._map.addLayer(this._markerGroup); + } + }, + + removeHooks: function () { + if (this._poly._map) { + this._poly._map.removeLayer(this._markerGroup); + delete this._markerGroup; + delete this._markers; + } + }, + + updateMarkers: function () { + this._markerGroup.clearLayers(); + this._initMarkers(); + }, + + _initMarkers: function () { + if (!this._markerGroup) { + this._markerGroup = new L.LayerGroup(); + } + this._markers = []; + + var latlngs = this._poly._latlngs, + i, j, len, marker; + + // TODO refactor holes implementation in Polygon to support it here + + for (i = 0, len = latlngs.length; i < len; i++) { + + marker = this._createMarker(latlngs[i], i); + marker.on('click', this._onMarkerClick, this); + this._markers.push(marker); + } + + var markerLeft, markerRight; + + for (i = 0, j = len - 1; i < len; j = i++) { + if (i === 0 && !(L.Polygon && (this._poly instanceof L.Polygon))) { + continue; + } + + markerLeft = this._markers[j]; + markerRight = this._markers[i]; + + this._createMiddleMarker(markerLeft, markerRight); + this._updatePrevNext(markerLeft, markerRight); + } + }, + + _createMarker: function (latlng, index) { + var marker = new L.Marker(latlng, { + draggable: true, + icon: this.options.icon + }); + + marker._origLatLng = latlng; + marker._index = index; + + marker.on('drag', this._onMarkerDrag, this); + marker.on('dragend', this._fireEdit, this); + + this._markerGroup.addLayer(marker); + + return marker; + }, + + _fireEdit: function () { + this._poly.fire('edit'); + }, + + _onMarkerDrag: function (e) { + var marker = e.target; + + L.extend(marker._origLatLng, marker._latlng); + + if (marker._middleLeft) { + marker._middleLeft.setLatLng(this._getMiddleLatLng(marker._prev, marker)); + } + if (marker._middleRight) { + marker._middleRight.setLatLng(this._getMiddleLatLng(marker, marker._next)); + } + + this._poly.redraw(); + }, + + _onMarkerClick: function (e) { + // we want to remove the marker on click, but if latlng count < 3, polyline would be invalid + if (this._poly._latlngs.length < 3) { return; } + + var marker = e.target, + i = marker._index; + + // remove the marker + this._markerGroup.removeLayer(marker); + this._markers.splice(i, 1); + this._poly.spliceLatLngs(i, 1); + this._updateIndexes(i, -1); + + // update prev/next links of adjacent markers + this._updatePrevNext(marker._prev, marker._next); + + // remove ghost markers near the removed marker + if (marker._middleLeft) { + this._markerGroup.removeLayer(marker._middleLeft); + } + if (marker._middleRight) { + this._markerGroup.removeLayer(marker._middleRight); + } + + // create a ghost marker in place of the removed one + if (marker._prev && marker._next) { + this._createMiddleMarker(marker._prev, marker._next); + + } else if (!marker._prev) { + marker._next._middleLeft = null; + + } else if (!marker._next) { + marker._prev._middleRight = null; + } + + this._poly.fire('edit'); + }, + + _updateIndexes: function (index, delta) { + this._markerGroup.eachLayer(function (marker) { + if (marker._index > index) { + marker._index += delta; + } + }); + }, + + _createMiddleMarker: function (marker1, marker2) { + var latlng = this._getMiddleLatLng(marker1, marker2), + marker = this._createMarker(latlng), + onClick, + onDragStart, + onDragEnd; + + marker.setOpacity(0.6); + + marker1._middleRight = marker2._middleLeft = marker; + + onDragStart = function () { + var i = marker2._index; + + marker._index = i; + + marker + .off('click', onClick) + .on('click', this._onMarkerClick, this); + + latlng.lat = marker.getLatLng().lat; + latlng.lng = marker.getLatLng().lng; + this._poly.spliceLatLngs(i, 0, latlng); + this._markers.splice(i, 0, marker); + + marker.setOpacity(1); + + this._updateIndexes(i, 1); + marker2._index++; + this._updatePrevNext(marker1, marker); + this._updatePrevNext(marker, marker2); + }; + + onDragEnd = function () { + marker.off('dragstart', onDragStart, this); + marker.off('dragend', onDragEnd, this); + + this._createMiddleMarker(marker1, marker); + this._createMiddleMarker(marker, marker2); + }; + + onClick = function () { + onDragStart.call(this); + onDragEnd.call(this); + this._poly.fire('edit'); + }; + + marker + .on('click', onClick, this) + .on('dragstart', onDragStart, this) + .on('dragend', onDragEnd, this); + + this._markerGroup.addLayer(marker); + }, + + _updatePrevNext: function (marker1, marker2) { + if (marker1) { + marker1._next = marker2; + } + if (marker2) { + marker2._prev = marker1; + } + }, + + _getMiddleLatLng: function (marker1, marker2) { + var map = this._poly._map, + p1 = map.latLngToLayerPoint(marker1.getLatLng()), + p2 = map.latLngToLayerPoint(marker2.getLatLng()); + + return map.layerPointToLatLng(p1._add(p2)._divideBy(2)); + } +}); + +L.Polyline.addInitHook(function () { + + if (L.Handler.PolyEdit) { + this.editing = new L.Handler.PolyEdit(this); + + if (this.options.editable) { + this.editing.enable(); + } + } + + this.on('add', function () { + if (this.editing && this.editing.enabled()) { + this.editing.addHooks(); + } + }); + + this.on('remove', function () { + if (this.editing && this.editing.enabled()) { + this.editing.removeHooks(); + } + }); +}); + + +/* + * 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; + }, + + 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; + } +}); + +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'); + } +}); + + +/* + * L.Control.Zoom is used for the default zoom buttons on the map. + */ + +L.Control.Zoom = L.Control.extend({ + options: { + position: 'topleft' + }, + + onAdd: function (map) { + var zoomName = 'leaflet-control-zoom', + barName = 'leaflet-bar', + partName = barName + '-part', + container = L.DomUtil.create('div', zoomName + ' ' + barName); + + this._map = map; + + this._zoomInButton = this._createButton('+', 'Zoom in', + zoomName + '-in ' + + partName + ' ' + + partName + '-top', + container, this._zoomIn, this); + + this._zoomOutButton = this._createButton('-', 'Zoom out', + zoomName + '-out ' + + partName + ' ' + + partName + '-bottom', + container, this._zoomOut, this); + + map.on('zoomend', this._updateDisabled, this); + + return container; + }, + + onRemove: function (map) { + map.off('zoomend', 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); + + return link; + }, + + _updateDisabled: function () { + var map = this._map, + className = 'leaflet-control-zoom-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: 'Powered by Leaflet' + }, + + 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); + + 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; } + + this._attributions[text]--; + this._update(); + + return this; + }, + + _update: function () { + if (!this._map) { return; } + + var attribs = []; + + for (var i in this._attributions) { + if (this._attributions.hasOwnProperty(i) && 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) { + if (baseLayers.hasOwnProperty(i)) { + this._addLayer(baseLayers[i], i); + } + } + + for (i in overlays) { + if (overlays.hasOwnProperty(i)) { + 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) + .off('layerremove', this._onLayerChange); + }, + + 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); + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(container); + L.DomEvent.on(container, 'mousewheel', L.DomEvent.stopPropagation); + } else { + L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation); + } + + var form = this._form = L.DomUtil.create('form', className + '-list'); + + if (this.options.collapsed) { + 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.stopPropagation) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', this._expand, this); + } + else { + L.DomEvent.on(link, 'focus', this._expand, this); + } + + this._map.on('movestart', 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; + + for (var i in this._layers) { + if (this._layers.hasOwnProperty(i)) { + var 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 id = L.stamp(e.layer); + + if (this._layers[id] && !this._handlingClick) { + this._update(); + } + }, + + // 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 = ' 0.5 && + this._getLoadedTilesPercentage(tilePane) < 0.5) { + + tilePane.style.visibility = 'hidden'; + tilePane.empty = true; + this._stopLoadingImages(tilePane); + return; + } + + if (!tileBg) { + tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane); + tileBg.style.zIndex = 1; + } + + // prepare the background pane to become the main tile pane + tileBg.style[L.DomUtil.TRANSFORM] = ''; + tileBg.style.visibility = 'hidden'; + + // tells tile layers to reinitialize their containers + tileBg.empty = true; //new FG + tilePane.empty = false; //new BG + + //Switch out the current layer to be the new bg layer (And vice-versa) + this._tilePane = this._panes.tilePane = tileBg; + var newTileBg = this._tileBg = tilePane; + + L.DomUtil.addClass(newTileBg, 'leaflet-zoom-animated'); + + this._stopLoadingImages(newTileBg); + }, + + _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); + } + } + }, + + _onZoomTransitionEnd: function () { + this._restoreTileFront(); + + L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); + L.Util.falseFn(this._tileBg.offsetWidth); // force reflow + this._animatingZoom = false; + this._resetView(this._animateToCenter, this._animateToZoom, true, true); + + if (L.Draggable) { + L.Draggable._disabled = false; + } + }, + + _restoreTileFront: function () { + this._tilePane.innerHTML = ''; + this._tilePane.style.visibility = ''; + this._tilePane.style.zIndex = 2; + this._tileBg.style.zIndex = 1; + }, + + _clearTileBg: function () { + if (!this._animatingZoom && !this.touchZoom._zooming) { + this._tileBg.innerHTML = ''; + } + } +}); + + +/* + * 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._locationOptions = 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); + } + return this; + }, + + _handleGeolocationError: function (error) { + var c = error.code, + message = error.message || + (c === 1 ? "permission denied" : + (c === 2 ? "position unavailable" : "timeout")); + + if (this._locationOptions.setView && !this._loaded) { + this.fitWorld(); + } + + this.fire('locationerror', { + code: c, + message: "Geolocation error: " + message + "." + }); + }, + + _handleGeolocationResponse: function (pos) { + var latAccuracy = 180 * pos.coords.accuracy / 4e7, + lngAccuracy = latAccuracy * 2, + + lat = pos.coords.latitude, + lng = pos.coords.longitude, + latlng = new L.LatLng(lat, lng), + + sw = new L.LatLng(lat - latAccuracy, lng - lngAccuracy), + ne = new L.LatLng(lat + latAccuracy, lng + lngAccuracy), + bounds = new L.LatLngBounds(sw, ne), + + options = this._locationOptions; + + if (options.setView) { + var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom); + this.setView(latlng, zoom); + } + + this.fire('locationfound', { + latlng: latlng, + bounds: bounds, + accuracy: pos.coords.accuracy + }); + } +}); + + +}(this, document)); diff --git a/chimere/templates/chimere/base.html b/chimere/templates/chimere/base.html index 620c84f..0a5818f 100644 --- a/chimere/templates/chimere/base.html +++ b/chimere/templates/chimere/base.html @@ -3,8 +3,8 @@ {% block extra_head %} -{% if css_area %} -{% endif %} +{% if css_map %} +{% endif %} {% endblock %} {% block header %}