diff --git a/static/index.html b/static/index.html
index 99d3704..c1d126f 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,6 +1,7 @@
+
Wildlife Map
+
+
+
+
+
+
+
+
-
-
+
diff --git a/static/sidebar/easy-button.css b/static/sidebar/easy-button.css
new file mode 100644
index 0000000..b654071
--- /dev/null
+++ b/static/sidebar/easy-button.css
@@ -0,0 +1,56 @@
+.leaflet-bar button,
+.leaflet-bar button:hover {
+ background-color: #fff;
+ border: none;
+ border-bottom: 1px solid #ccc;
+ width: 26px;
+ height: 26px;
+ line-height: 26px;
+ display: block;
+ text-align: center;
+ text-decoration: none;
+ color: black;
+}
+
+.leaflet-bar button {
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ overflow: hidden;
+ display: block;
+}
+
+.leaflet-bar button:hover {
+ background-color: #f4f4f4;
+}
+
+.leaflet-bar button:first-of-type {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+}
+
+.leaflet-bar button:last-of-type {
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-bottom: none;
+}
+
+.leaflet-bar.disabled,
+.leaflet-bar button.disabled {
+ cursor: default;
+ pointer-events: none;
+ opacity: .4;
+}
+
+.easy-button-button .button-state{
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+
+.leaflet-touch .leaflet-bar button {
+ width: 30px;
+ height: 30px;
+ line-height: 30px;
+}
\ No newline at end of file
diff --git a/static/sidebar/easy-button.js b/static/sidebar/easy-button.js
new file mode 100644
index 0000000..7671a78
--- /dev/null
+++ b/static/sidebar/easy-button.js
@@ -0,0 +1,376 @@
+(function(){
+
+// This is for grouping buttons into a bar
+// takes an array of `L.easyButton`s and
+// then the usual `.addTo(map)`
+L.Control.EasyBar = L.Control.extend({
+
+ options: {
+ position: 'topleft', // part of leaflet's defaults
+ id: null, // an id to tag the Bar with
+ leafletClasses: true // use leaflet classes?
+ },
+
+
+ initialize: function(buttons, options){
+
+ if(options){
+ L.Util.setOptions( this, options );
+ }
+
+ this._buildContainer();
+ this._buttons = [];
+
+ for(var i = 0; i < buttons.length; i++){
+ buttons[i]._bar = this;
+ buttons[i]._container = buttons[i].button;
+ this._buttons.push(buttons[i]);
+ this.container.appendChild(buttons[i].button);
+ }
+
+ },
+
+
+ _buildContainer: function(){
+ this._container = this.container = L.DomUtil.create('div', '');
+ this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control');
+ this.options.id && (this.container.id = this.options.id);
+ },
+
+
+ enable: function(){
+ L.DomUtil.addClass(this.container, 'enabled');
+ L.DomUtil.removeClass(this.container, 'disabled');
+ this.container.setAttribute('aria-hidden', 'false');
+ return this;
+ },
+
+
+ disable: function(){
+ L.DomUtil.addClass(this.container, 'disabled');
+ L.DomUtil.removeClass(this.container, 'enabled');
+ this.container.setAttribute('aria-hidden', 'true');
+ return this;
+ },
+
+
+ onAdd: function () {
+ return this.container;
+ },
+
+ addTo: function (map) {
+ this._map = map;
+
+ for(var i = 0; i < this._buttons.length; i++){
+ this._buttons[i]._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;
+ }
+
+});
+
+L.easyBar = function(){
+ var args = [L.Control.EasyBar];
+ for(var i = 0; i < arguments.length; i++){
+ args.push( arguments[i] );
+ }
+ return new (Function.prototype.bind.apply(L.Control.EasyBar, args));
+};
+
+// L.EasyButton is the actual buttons
+// can be called without being grouped into a bar
+L.Control.EasyButton = L.Control.extend({
+
+ options: {
+ position: 'topleft', // part of leaflet's defaults
+
+ id: null, // an id to tag the button with
+
+ type: 'replace', // [(replace|animate)]
+ // replace swaps out elements
+ // animate changes classes with all elements inserted
+
+ states: [], // state names look like this
+ // {
+ // stateName: 'untracked',
+ // onClick: function(){ handle_nav_manually(); };
+ // title: 'click to make inactive',
+ // icon: 'fa-circle', // wrapped with
+ // }
+
+ leafletClasses: true, // use leaflet styles for the button
+ tagName: 'button',
+ },
+
+
+
+ initialize: function(icon, onClick, title, id){
+
+ // clear the states manually
+ this.options.states = [];
+
+ // add id to options
+ if(id != null){
+ this.options.id = id;
+ }
+
+ // storage between state functions
+ this.storage = {};
+
+ // is the last item an object?
+ if( typeof arguments[arguments.length-1] === 'object' ){
+
+ // if so, it should be the options
+ L.Util.setOptions( this, arguments[arguments.length-1] );
+ }
+
+ // if there aren't any states in options
+ // use the early params
+ if( this.options.states.length === 0 &&
+ typeof icon === 'string' &&
+ typeof onClick === 'function'){
+
+ // turn the options object into a state
+ this.options.states.push({
+ icon: icon,
+ onClick: onClick,
+ title: typeof title === 'string' ? title : ''
+ });
+ }
+
+ // curate and move user's states into
+ // the _states for internal use
+ this._states = [];
+
+ for(var i = 0; i < this.options.states.length; i++){
+ this._states.push( new State(this.options.states[i], this) );
+ }
+
+ this._buildButton();
+
+ this._activateState(this._states[0]);
+
+ },
+
+ _buildButton: function(){
+
+ this.button = L.DomUtil.create(this.options.tagName, '');
+
+ if (this.options.tagName === 'button') {
+ this.button.setAttribute('type', 'button');
+ }
+
+ if (this.options.id ){
+ this.button.id = this.options.id;
+ }
+
+ if (this.options.leafletClasses){
+ L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part leaflet-interactive');
+ }
+
+ // don't let double clicks and mousedown get to the map
+ L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop);
+ L.DomEvent.addListener(this.button, 'mousedown', L.DomEvent.stop);
+ L.DomEvent.addListener(this.button, 'mouseup', L.DomEvent.stop);
+
+ // take care of normal clicks
+ L.DomEvent.addListener(this.button,'click', function(e){
+ L.DomEvent.stop(e);
+ this._currentState.onClick(this, this._map ? this._map : null );
+ this._map && this._map.getContainer().focus();
+ }, this);
+
+ // prep the contents of the control
+ if(this.options.type == 'replace'){
+ this.button.appendChild(this._currentState.icon);
+ } else {
+ for(var i=0;i"']/) ){
+
+ // if so, the user should have put in html
+ // so move forward as such
+ tmpIcon = ambiguousIconString;
+
+ // then it wasn't html, so
+ // it's a class list, figure out what kind
+ } else {
+ ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,'');
+ tmpIcon = L.DomUtil.create('span', '');
+
+ if( ambiguousIconString.indexOf('fa-') === 0 ){
+ L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString)
+ } else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) {
+ L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString)
+ } else {
+ L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString)
+ }
+
+ // make this a string so that it's easy to set innerHTML below
+ tmpIcon = tmpIcon.outerHTML;
+ }
+
+ return tmpIcon;
+}
+
+})();
\ No newline at end of file
diff --git a/static/sidebar/filter.css b/static/sidebar/filter.css
new file mode 100644
index 0000000..6f1d6df
--- /dev/null
+++ b/static/sidebar/filter.css
@@ -0,0 +1,143 @@
+.leaflet-bar .tag-filter-tags-container * {margin: 0; padding: 0;}
+
+.leaflet-bar .tag-filter-tags-container {
+ display: none;
+ position: absolute;
+ top:0px;
+ z-index: 1000;
+ padding-bottom: 10px;
+ padding-left: 5px;
+}
+
+
+.leaflet-bar.easy-button-container.leaflet-control {
+ text-align: left !important;
+}
+
+.leaflet-bar span.filter-info-box {
+ position: absolute;
+ margin-top: -5px;
+ margin-left: 10px;
+ color: white;
+ font-size: 12px;
+ text-transform: uppercase;
+ padding: .2em .6em .3em;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: .25em;
+ background-color: #454b52;
+ box-sizing: inherit !important;
+}
+
+.leaflet-bar .tag-filter-tags-container ul {
+ border: 1px solid hsl(180, 40%, 60%);
+ box-shadow: 0 1px 7px #999;
+ width: 150px; margin: 0 auto;
+ overflow-y: auto;
+ max-height: 200px;
+ -webkit-border-bottom-right-radius: 5px;
+ -webkit-border-bottom-left-radius: 5px;
+ -moz-border-radius-bottomright: 5px;
+ -moz-border-radius-bottomleft: 5px;
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+}
+
+.leaflet-bar .tag-filter-tags-container ul li:last-child {
+ border: 0px;
+}
+
+.leaflet-bar .tag-filter-tags-container ul.header li:last-child {
+ border: 0px;
+}
+
+.leaflet-bar .tag-filter-tags-container ul.header {
+ height: 28px;
+ overflow: hidden;
+ border-top: 1px solid hsl(180, 40%, 60%);
+ border-bottom: 1px solid hsl(180, 40%, 60%);
+ -webkit-border-bottom-right-radius: 0px;
+ -webkit-border-bottom-left-radius: 0px;
+ -moz-border-radius-bottomright: 0px;
+ -moz-border-radius-bottomleft: 0px;
+ border-bottom-right-radius: 0px;
+ border-bottom-left-radius: 0px;
+ -webkit-border-top-left-radius: 5px;
+ -webkit-border-top-right-radius: 5px;
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+.leaflet-bar .tag-filter-tags-container ul.header li a {
+ padding: 0px;
+}
+
+.leaflet-bar .tag-filter-tags-container ul.header li {
+ text-align: center;
+}
+
+.leaflet-bar .tag-filter-tags-container ul li {
+ background: #fcfdff;
+ list-style-type: none;
+ position: relative;
+ overflow: hidden;
+ cursor: pointer;
+ border-bottom: 1px solid hsl(180, 40%, 60%);
+}
+
+.leaflet-bar .tag-filter-tags-container ul li a {
+ background-color: transparent !important;
+ font-size: 12px;
+ font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
+ color: hsl(180, 40%, 40%);
+ display: inline;
+ padding: 5px 15px;
+ text-decoration: none;
+ cursor: pointer;
+ user-select: none;
+ position: relative;
+}
+
+.leaflet-bar .tag-filter-tags-container .ink {
+ display: block; position: absolute;
+ background: hsl(180, 40%, 80%);
+ border-radius: 100%;
+ transform: scale(0);
+}
+
+.leaflet-bar .tag-filter-tags-container .checkbox {
+ display: none;
+ font-size: 14px;
+ color: hsl(180, 40%, 40%);
+ margin-left: 5px;
+}
+
+
+.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar {
+ width: 8px;
+}
+.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-button {
+ width: 8px;
+ height:5px;
+}
+.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-track {
+ background:#eee;
+ border: thin solid lightgray;
+ box-shadow: 0px 0px 3px #dfdfdf inset;
+ border-radius:10px;
+}
+.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-thumb {
+ background:#999;
+ border: thin solid gray;
+ border-radius:10px;
+}
+.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-thumb:hover {
+ background:#7d7d7d;
+}
+.leaflet-bar .tag-filter-tags-container a, .leaflet-bar .tag-filter-tags-container .checkbox {
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/static/sidebar/filter.js b/static/sidebar/filter.js
new file mode 100644
index 0000000..0a20612
--- /dev/null
+++ b/static/sidebar/filter.js
@@ -0,0 +1,473 @@
+(function() {
+
+ L.Control.TagFilterButton = L.Control.extend({
+
+ options: {
+ icon: "fa-filter", //buton icon default is fa-filter
+ onSelectionComplete: null, // the callback function for selected tags
+ data: null, // the data to be used for tags popup, it can be array or function
+ clearText: 'clear', // the text of the clear button
+ filterOnEveryClick: false, // if set as true the plugin do filtering operation on every click event on the checkboxes
+ openPopupOnHover: false, // if set as true the popup that contains tags will be open at mouse hover time
+
+ ajaxData: null // it can be used for remote data TODO: implement it!
+ },
+
+ _map: null,
+ _container: null,
+ _easyButton: null,
+ _tagEl: null,
+ _clearEl: null,
+ _filterInfo: null,
+ _selectedTags: [],
+ _invisibles: null,
+ _releatedFilterButtons: null,
+ layerSources: null,
+
+ // GLOBAL FUNCTIONS
+
+ /**
+ * @function: resetCaches
+ * Resets marker caches
+ * @param update: if send as true, the @update function is called after cleaning the cache
+ * */
+ resetCaches: function(update) {
+ if (typeof update !== 'boolean')
+ update = true;
+ this._invisibles = [];
+ if (update) {
+ this.update();
+ }
+ },
+
+ /**
+ * @function: update
+ * Update markers by last selected tags
+ *
+ * */
+ update: function() {
+ var filteredCount = this.layerSources.currentSource.hide.call(this, this.layerSources.currentSource);
+ this._showFilterInfo(filteredCount);
+ },
+
+ /**
+ * @function: hasFiltered
+ * returns true if any tag(s) selected otherwise false
+ *
+ * */
+ hasFiltered: function () {
+ return this._selectedTags.length > 0;
+ },
+
+ /**
+ * @function: registerCustomSource
+ * Register @source object for filtering markers by tags. If you want to use this function
+ * you must implement @hide function.
+ * @param source reference of the new marker source. It must have name and source item
+ *
+ * */
+ registerCustomSource: function(source) {
+ if (source.name && source.source && typeof source.source.hide === 'function') {
+ this.layerSources.sources[source.name] = source.source;
+ } else {
+ throw 'Layer source is incompatible';
+ }
+ },
+
+ /**
+ * @function: enablePruneCluster
+ * @param pruneClusterInstance adds pruneCluster reference to layersources
+ *
+ * */
+ enablePruneCluster: function (pruneClusterInstance) {
+ this.registerCustomSource({
+ "name": "pruneCluster",
+ "source": {
+ pruneCluster: pruneClusterInstance,
+ hide: function(layerSource) {
+ var toBeRemovedFromInvisibles = [], i, j;
+
+ for (i = 0; i < this._invisibles.length; i++) {
+ for (j = 0; j < this._invisibles[i].data.tags.length; j++) {
+ if (this._selectedTags.length == 0 || this._selectedTags.indexOf(this._invisibles[i].data.tags[j]) !== -1) {
+ layerSource.pruneCluster.RegisterMarker(this._invisibles[i]);
+ toBeRemovedFromInvisibles.push(i);
+ break;
+ }
+ }
+ }
+
+ while(toBeRemovedFromInvisibles.length > 0) {
+ this._invisibles.splice(toBeRemovedFromInvisibles.pop(), 1);
+ }
+
+ var removedMarkers = [];
+ var totalCount = 0;
+
+ if (this._selectedTags.length > 0) {
+
+ var releatedLayers = [];
+
+ for (var r = 0; r < this._releatedFilterButtons.length; r++) {
+ releatedLayers = releatedLayers.concat(this._releatedFilterButtons[r].getInvisibles());
+ }
+
+ var markers = layerSource.pruneCluster.GetMarkers();
+ for (i = 0; i < markers.length; i++) {
+ if (releatedLayers.indexOf(markers[i]) == -1 && markers[i].data && markers[i].data.tags) {
+ totalCount++;
+ var found = false;
+ for (var j = 0; j < markers[i].data.tags.length; j++) {
+ found = this._selectedTags.indexOf(markers[i].data.tags[j]) !== -1;
+ if (found) {
+ break;
+ }
+ }
+ if (!found) {
+ removedMarkers.push(markers[i]);
+ }
+ }
+ }
+
+ for (i = 0; i < removedMarkers.length; i++) {
+ this._invisibles.push(removedMarkers[i]);
+ }
+
+ layerSource.pruneCluster.RemoveMarkers(removedMarkers);
+
+ }
+
+
+ layerSource.pruneCluster.ProcessView();
+
+ return totalCount - removedMarkers.length;
+ }
+ }
+ });
+
+ this.layerSources.currentSource = this.layerSources.sources["pruneCluster"];
+ },
+
+ /**
+ * @function: addToReleated
+ * @param other adds another linked tagFilterButton reference to _releatedFilterButtons
+ *
+ * */
+ addToReleated: function(other) {
+ if (other && other instanceof L.Control.TagFilterButton && this._releatedFilterButtons.indexOf(other) == -1) {
+ this._releatedFilterButtons.push(other);
+ return other.addToReleated(this);
+ }
+ console.error("could not add tagFilterButton instance to releated");
+ return false;
+ },
+
+ /**
+ * @function: getInvisibles
+ * @param other gets invisibles layers hiding by this plugin
+ *
+ * */
+ getInvisibles: function() {
+ return this._invisibles;
+ },
+
+ _prepareLayerSources: function() {
+
+ this.layerSources = new Object();
+ this.layerSources["sources"] = new Object();
+
+ this.registerCustomSource({
+ "name": "default",
+ "source": {
+ hide: function() {
+
+ var releatedLayers = [];
+
+ for (var r = 0; r < this._releatedFilterButtons.length; r++) {
+ releatedLayers = releatedLayers.concat(this._releatedFilterButtons[r].getInvisibles());
+ }
+
+ var toBeRemovedFromInvisibles = [], i;
+
+ for (i = 0; i < this._invisibles.length; i++) {
+ if (releatedLayers.indexOf(this._invisibles[i]) == -1) {
+ for (j = 0; j < this._invisibles[i].options.tags.length; j++) {
+ if (this._selectedTags.length == 0 || this._selectedTags.indexOf(this._invisibles[i].options.tags[j]) !== -1) {
+ this._map.addLayer(this._invisibles[i]);
+ toBeRemovedFromInvisibles.push(i);
+ break;
+ }
+ }
+ }
+ }
+
+ while(toBeRemovedFromInvisibles.length > 0) {
+ this._invisibles.splice(toBeRemovedFromInvisibles.pop(), 1);
+ }
+
+ var removedMarkers = [];
+ var totalCount = 0;
+
+ if (this._selectedTags.length > 0) {
+
+ this._map.eachLayer(function(layer) {
+ if (layer && layer.options && layer.options.tags) {
+ totalCount++;
+ if (releatedLayers.indexOf(layer) == -1) {
+ var found = false;
+ for (var i = 0; i < layer.options.tags.length; i++) {
+ found = this._selectedTags.indexOf(layer.options.tags[i]) !== -1;
+ if (found) {
+ break;
+ }
+ }
+ if (!found) {
+ removedMarkers.push(layer);
+ }
+ }
+ }
+ }.bind(this));
+
+ for (i = 0; i < removedMarkers.length; i++) {
+ this._map.removeLayer(removedMarkers[i]);
+ this._invisibles.push(removedMarkers[i]);
+ }
+
+ }
+
+ return totalCount - removedMarkers.length;
+ }
+ }
+ });
+ this.layerSources.currentSource = this.layerSources.sources["default"];
+ },
+
+ _showFilterInfo: function(filteredCount) {
+ if (this._selectedTags.length > 0) {
+ this._filterInfo.innerText = filteredCount.toString();
+ this._filterInfo.style.display = "";
+ } else {
+ this._filterInfo.style.display = "none";
+ }
+ },
+
+ _checkItem: function(item) {
+ item.getElementsByClassName('checkbox')[0].style.display = "inline-block";
+ item.dataset.checked = "checked";
+ },
+
+ _uncheckItem: function(item) {
+ item.dataset.checked = "";
+ item.getElementsByClassName('checkbox')[0].style.display = "none";
+ },
+
+ _onClickToItem: function(e) {
+ L.DomEvent.stop(e);
+ var li = this.element;
+ var context = this.context;
+ if (!li.dataset.checked) {
+ context._checkItem(li);
+ } else {
+ context._uncheckItem(li);
+ }
+ if (context.options.filterOnEveryClick) {
+ context.filter.call(context);
+ }
+ },
+
+ _preparePopup: function(data) {
+
+ this._tagEl.innerHTML = '';
+
+ for (var i = 0; i < data.length; i++) {
+ var li = L.DomUtil.create('li', 'ripple', this._tagEl);
+ var checkbox = L.DomUtil.create('span', 'checkbox', li);
+ var a = L.DomUtil.create('a', '', li);
+ var text = data[i];
+ var value = data[i];
+ if (typeof text == 'object' && Object.keys(data[i]).length == 2) { // key,value
+ text = data[i].name;
+ value = data[i].value;
+ }
+
+ var checked = this._selectedTags.indexOf(value) !== -1;
+ if (checked) {
+ checkbox.style.display = "inline-block";
+ li.dataset.checked = "checked";
+ }
+ checkbox.innerHTML= "✔";
+ li.dataset.value = value;
+ a.innerText = text;
+
+ L.DomEvent.addListener(li, 'dblclick', this._onClickToItem.bind({ context: this, element: li }));
+ L.DomEvent.addListener(li, 'click', this._onClickToItem.bind({ context: this, element: li }));
+
+ L.DomEvent.addListener(a, 'dblclick', function(e) {
+ L.DomEvent.stop(e);
+ this.context._onClickToItem.call(this, e);
+ }.bind({ context: this, element: li }));
+
+ L.DomEvent.addListener(a, 'click', function(e) {
+ L.DomEvent.stop(e);
+ this.context._onClickToItem.call(this, e);
+ }.bind({ context: this, element: li }));
+
+
+ }
+ this._container.style.display = "block";
+ },
+
+ _clearSelections: function(e) {
+ L.DomEvent.stop(e);
+ this._selectedTags = [];
+ var childCount = this._tagEl.childElementCount,
+ children = this._tagEl.children,
+ childCheckbox, i;
+
+ this._selectedTags = [];
+
+ for (i = 0; i < childCount; i++) {
+ childCheckbox = children[i];
+ if (childCheckbox) {
+ this._uncheckItem(childCheckbox);
+ }
+ }
+
+ if (this.options.filterOnEveryClick) {
+ this.filter();
+ }
+ },
+
+ _showTagFilterPopup: function() {
+
+ if (this._tagFilterPopupIsOpen()) {
+ return;
+ }
+
+ this._easyButton.button.style.display = "none";
+ this._filterInfo.style.display = "none";
+
+ if (!this._container) {
+ throw 'container is not initialized!';
+ }
+
+ if (!this.options.data && !this.options.ajaxData) {
+ throw 'data is empty!';
+ }
+
+ if (this.options.data) {
+ if (typeof this.options.data === 'function') {
+ this._preparePopup(this.options.data());
+ } else {
+ this._preparePopup(this.options.data);
+ }
+ }
+
+ },
+
+ _tagFilterPopupIsOpen: function() {
+ return this._container.style.display == 'block';
+ },
+
+ filter: function(withTags) {
+ var checkboxContainer = (this._container.getElementsByTagName('div')[0]),
+ childCount = this._tagEl.childElementCount,
+ children = this._tagEl.children,
+ childCheckbox, i, j;
+
+ this._selectedTags = [];
+
+ if (withTags) {
+ var acceptingTags = [];
+ for (i = 0; i < withTags.length; i++) {
+ if (this.options.data.indexOf(withTags[i]) !== -1) {
+ acceptingTags.push(withTags[i]);
+ }
+ }
+ withTags = acceptingTags;
+ }
+
+ if (!withTags || !withTags.length) {
+ for (i = 0; i < childCount; i++) {
+ childCheckbox = children[i];
+ if (childCheckbox && childCheckbox.dataset.checked) {
+ this._selectedTags.push(childCheckbox.dataset.value);
+ }
+ }
+ } else {
+ this._selectedTags = withTags;
+ }
+
+ var filteredCount = this.layerSources.currentSource.hide.call(this, this.layerSources.currentSource);
+ this._showFilterInfo(filteredCount);
+
+ if (this.options.onSelectionComplete && typeof this.options.onSelectionComplete == 'function') {
+ this.options.onSelectionComplete.call(this, this._selectedTags);
+ }
+ },
+
+ hide: function(accept) {
+ if (this._container && (this._container.style.display == "none" || this._container.style.display == "")) {
+ return;
+ }
+ if (this._container) {
+ this._container.style.display = "none";
+ }
+ this.filter();
+ this._easyButton.button.style.display = "block";
+ },
+
+ initialize: function(options) {
+ this._invisibles = [];
+ this._releatedFilterButtons = [];
+ L.Util.setOptions(this, options || {});
+ this._prepareLayerSources();
+ },
+
+ addTo: function(map) {
+ this._map = map;
+ if (this.options.openPopupOnHover) {
+ this._easyButton = L.easyButton(this.options.icon, function() {
+ }).addTo(map);
+ L.DomEvent.addListener(this._easyButton._container, 'mouseover', this._showTagFilterPopup.bind(this));
+ } else {
+ this._easyButton = L.easyButton(this.options.icon, this._showTagFilterPopup.bind(this)).addTo(map);
+ }
+ this._container = L.DomUtil.create('div', 'tag-filter-tags-container', this._easyButton._container);
+
+ if (!L.Browser.touch) {
+ L.DomEvent.disableClickPropagation(this._container);
+ L.DomEvent.disableScrollPropagation(this._container);
+ } else {
+ L.DomEvent.disableClickPropagation(this._container);
+ L.DomEvent.disableScrollPropagation(this._container);
+ }
+
+ this._filterInfo = L.DomUtil.create('span', 'filter-info-box', this._easyButton._container);
+ this._showFilterInfo(0);
+
+ this._clearEl = L.DomUtil.create('ul', 'header', this._container);
+ this._clearEl.innerHTML = "" + this.options.clearText + "";
+
+ L.DomEvent.addListener(this._clearEl.getElementsByTagName('a')[0], 'click', this._clearSelections.bind(this));
+ L.DomEvent.addListener(this._clearEl.getElementsByTagName('li')[0], 'click', this._clearSelections.bind(this));
+
+ this._tagEl = L.DomUtil.create('ul', '', this._container);
+ this._map.on('dragstart click', this.hide, this);
+ L.DomEvent.addListener(this._container, 'dblclick', L.DomEvent.stop);
+ L.DomEvent.addListener(this._container, 'click', L.DomEvent.stop);
+ return this;
+ },
+
+ onRemove: function(map) {
+ this._container.parentNode.removeChild(this._container);
+ return this;
+ }
+
+ });
+
+ L.control.tagFilterButton = function(options) {
+ return new L.Control.TagFilterButton(options);
+ };
+
+}).call(this);
\ No newline at end of file
diff --git a/static/sidebar/search.css b/static/sidebar/search.css
new file mode 100644
index 0000000..8048d33
--- /dev/null
+++ b/static/sidebar/search.css
@@ -0,0 +1,129 @@
+.leaflet-searchbox-wrapper {
+ display: flex;
+ justify-content: right;
+ height: 40px;
+ border-top-left-radius: 20px 50%;
+ border-bottom-left-radius: 20px 50%;
+ border-top-right-radius: 20px 50%;
+ border-bottom-right-radius: 20px 50%;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox,
+.leaflet-searchbox-wrapper .leaflet-searchbox-button,
+.leaflet-searchbox-container .leaflet-searchbox-autocomplete,
+.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item {
+ background-color: white;
+ border: 1px rgb(0, 0, 0, 0.3) solid;
+ outline: none;
+ transition-duration: 0.2s;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox {
+ width: 300px;
+ font-size: 17px;
+ transition-delay: 0.1s;
+ z-index: 702;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-left {
+ border-top-left-radius: 20px 50%;
+ border-bottom-left-radius: 20px 50%;
+ border-right: none;
+ padding: 0px 0px 0px 15px;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-right {
+ border-top-right-radius: 20px 50%;
+ border-bottom-right-radius: 20px 50%;
+ border-left: none;
+ padding: 0px 15px 0px 0px;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-button {
+ width: 40px;
+ transition-delay: 0s;
+ z-index: 702;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-button:hover {
+ cursor: pointer;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-button-right {
+ border-top-right-radius: 20px 50%;
+ border-bottom-right-radius: 20px 50%;
+ border-left: none;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-button-left {
+ border-top-left-radius: 20px 50%;
+ border-bottom-left-radius: 20px 50%;
+ border-right: none;
+}
+
+.leaflet-searchbox-wrapper .leaflet-searchbox-button i {
+ font-size: 130%;
+}
+
+.leaflet-searchbox-container .leaflet-searchbox-icon {
+ width: 69%;
+ height: auto;
+}
+
+.leaflet-searchbox-container .leaflet-searchbox-autocomplete {
+ position: absolute;
+ top: 20px;
+ width: 100%;
+ list-style-type: none;
+ margin: 0px;
+ padding: 20px 0px 0px 0px;
+ background-color: white;
+ border-top: none;
+ z-index: 701;
+}
+
+.leaflet-searchbox-container .leaflet-searchbox-autocomplete:empty {
+ display: none;
+}
+
+.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item {
+ width: 100%;
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ z-index: 701;
+ font-size: 15px;
+ padding: 3px 10px;
+ cursor: pointer;
+}
+
+.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item:hover {
+ background-color: #f3f3f3;
+}
+
+.collapsed .leaflet-searchbox {
+ width: 0px !important;
+ padding: 0px;
+ transition-delay: 0s;
+}
+
+.collapsed .leaflet-searchbox {
+ border-left: none;
+ border-right: none;
+}
+
+.collapsed .leaflet-searchbox-button {
+ border-radius: 50%;
+ transition-delay: 0.2s;
+ border: 1px rgb(0, 0, 0, 0.3) solid;
+}
+
+.collapsed .leaflet-searchbox-autocomplete {
+ display: none;
+}
+
+.leaflet-searchbox-wrapper.open .leaflet-searchbox,
+.leaflet-searchbox-wrapper.open .leaflet-searchbox-button {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+}
\ No newline at end of file
diff --git a/static/sidebar/search.js b/static/sidebar/search.js
new file mode 100644
index 0000000..afc2273
--- /dev/null
+++ b/static/sidebar/search.js
@@ -0,0 +1,264 @@
+(function (factory, window) {
+
+ // define an AMD module that relies on 'leaflet'
+ if (typeof define === 'function' && define.amd) {
+ define(['leaflet'], factory);
+
+ // define a Common JS module that relies on 'leaflet'
+ } else if (typeof exports === 'object') {
+ module.exports = factory(require('leaflet'));
+ }
+
+ // attach your plugin to the global 'L' variable
+ if (typeof window !== 'undefined' && window.L) {
+ window.L.YourPlugin = factory(L);
+ }
+}(function (L) {
+ L.Control.Searchbox = L.Control.extend({
+ options: {
+ class: '',
+ id: '',
+ position: 'topright',
+ expand: 'left',
+ collapsed: true,
+ width: null,
+ iconPath: 'img/search_icon.png',
+ autocompleteFeatures: ['setValueOnClick']
+ },
+
+ onAdd: function (map) {
+ this._create();
+
+ this._collapsed = this.options.collapsed;
+ if (this.options.collapsed) {
+ this.hide();
+ }
+
+ L.DomEvent.disableClickPropagation(this._container);
+
+ L.DomEvent.on(this._button, 'click', this._onClick, this);
+
+ // Autocomplete behaviour
+ if (this.options.autocompleteFeatures.includes('setValueOnClick')) {
+ this.onAutocomplete('click', function (e) {
+ this._onListItemClick(e.target);
+ });
+ }
+
+ return this._container;
+ },
+
+ onRemove: function (map) {
+
+ },
+
+
+ getValue: function () {
+ return this._input.value
+ },
+
+ setValue: function (value) {
+ this._input.value = value;
+ return this
+ },
+
+ addItem: function (item) {
+ var listItem = L.DomUtil.create('li', 'leaflet-searchbox-autocomplete-item', this._autocomplete);
+ listItem.textContent = item;
+ this._items.push(listItem);
+
+ L.DomUtil.addClass(this._searchboxWrapper, 'open');
+
+ return this
+ },
+
+ addItems: function (items) {
+ for (var i = 0; i < items.length; i++) {
+ this.addItem(items[i]);
+ }
+
+ return this
+ },
+
+ setItems: function (items) {
+ this.clearItems();
+ this.addItems(items);
+
+ return this
+ },
+
+ clearItems: function () {
+ this._autocomplete.innerHTML = '';
+ this._items = [];
+
+ L.DomUtil.removeClass(this._searchboxWrapper, 'open');
+
+ return this
+ },
+
+ hide: function () {
+ L.DomUtil.addClass(this._container, "collapsed");
+ this._input.blur();
+ this._button.blur();
+ setTimeout(() => {
+ this._collapsed = true;
+ }, 600);
+
+ return this;
+ },
+
+ show: function () {
+ L.DomUtil.removeClass(this._container, "collapsed");
+ setTimeout(() => {
+ this._collapsed = false;
+ }, 600);
+
+ return this;
+ },
+
+ toggle: function () {
+ if (L.DomUtil.hasClass(this._container, "collapsed")) {
+ this.show();
+ } else {
+ this.hide();
+ }
+
+ return this;
+ },
+
+ isCollapsed: function () {
+ return L.DomUtil.hasClass(this._container, "collapsed")
+ },
+
+ clearInput: function () {
+ this._input.value = '';
+
+ return this
+ },
+
+ clear: function () {
+ this.clearInput();
+ this.clearItems();
+
+ return this;
+ },
+
+ onInput: function (event, handler) {
+ L.DomEvent.on(this._input, event, handler, this);
+
+ return this
+ },
+
+ offInput: function (event, handler) {
+ L.DomEvent.off(this._input, event, handler, this);
+
+ return this
+ },
+
+ onButton: function (event, handler) {
+ var wrapper = this._buttonHandlerWrapper(handler);
+ L.DomEvent.on(this._button, event, wrapper, this);
+
+ return this
+ },
+
+ offButton: function (event, handler) {
+ var wrapper = this._buttonHandlerWrapper(handler);
+ L.DomEvent.off(this._button, event, wrapper, this);
+
+ return this
+ },
+
+ onAutocomplete: function (event, handler) {
+ L.DomEvent.on(this._autocomplete, event, handler, this);
+
+ return this
+ },
+
+ offAutocomplete: function (event, handler) {
+ L.DomEvent.off(this._autocomplete, event, handler, this);
+
+ return this
+ },
+
+ _onClick: function () {
+ if (this._collapsed) {
+ this.show();
+ this._input.focus();
+ }
+ },
+
+ _onListItemClick: function (item) {
+ this.setValue(item.innerHTML);
+ this._input.focus();
+ },
+
+ _buttonHandlerWrapper: function (handler) {
+ return function () {
+ if (!this._collapsed) {
+ handler();
+ }
+ }
+ },
+
+ _create: function () {
+ this._container = L.DomUtil.create('div', 'leaflet-control leaflet-searchbox-container');
+ if (this.options.class != '') {
+ L.DomUtil.addClass(this._container, this.options.class);
+ }
+ if (this.options.id != '') {
+ this._container.id = this.options.id;
+ }
+
+ this._searchboxWrapper = L.DomUtil.create('div', 'leaflet-searchbox-wrapper', this._container);
+
+ if (this.options.expand == 'left') {
+ this._createInput('left');
+ this._createButton('right');
+ } else if (this.options.expand == 'right') {
+ this._createButton('left');
+ this._createInput('right');
+ }
+ this._createAutocomplete();
+ },
+
+ _createInput: function (position) {
+ this._input = L.DomUtil.create(
+ 'input',
+ 'leaflet-searchbox leaflet-searchbox-' + position,
+ this._searchboxWrapper);
+ this._input.setAttribute('type', 'text');
+ if (this.options.width != null) {
+ this._input.style.width = this.options.width;
+ }
+ },
+
+ _createButton: function (position) {
+ this._button = L.DomUtil.create(
+ 'button',
+ 'leaflet-searchbox-button leaflet-searchbox-button-' + position,
+ this._searchboxWrapper);
+ this._button.setAttribute('type', 'button');
+ this._button.style.width = this.options.height;
+ this._button.style.height = this.options.height;
+ this._icon = L.DomUtil.create('img', 'leaflet-searchbox-icon', this._button);
+ this._icon.setAttribute('src', this.options.iconPath);
+ },
+
+ _createAutocomplete: function () {
+ this._autocomplete = L.DomUtil.create(
+ 'ul',
+ 'leaflet-searchbox-autocomplete',
+ this._container);
+
+ this._items = [];
+
+ }
+ });
+
+ return L.Control.Searchbox;
+}, window));
+
+L.control.searchbox = function (options) {
+ return new L.Control.Searchbox(options);
+}
diff --git a/static/sidebar/sidebar.css b/static/sidebar/sidebar.css
new file mode 100644
index 0000000..6e41c4c
--- /dev/null
+++ b/static/sidebar/sidebar.css
@@ -0,0 +1,198 @@
+.leaflet-sidebar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ overflow: hidden;
+ z-index: 2000; }
+ .leaflet-sidebar.collapsed {
+ width: 40px; }
+ @media (min-width: 768px) {
+ .leaflet-sidebar {
+ top: 10px;
+ bottom: 10px;
+ transition: width 500ms; } }
+ @media (min-width: 768px) and (max-width: 991px) {
+ .leaflet-sidebar {
+ width: 305px;
+ max-width: 305px; } }
+ @media (min-width: 992px) and (max-width: 1199px) {
+ .leaflet-sidebar {
+ width: 390px;
+ max-width: 390px; } }
+ @media (min-width: 1200px) {
+ .leaflet-sidebar {
+ width: 460px;
+ max-width: 460px; } }
+
+.leaflet-sidebar-left {
+ left: 0; }
+ @media (min-width: 768px) {
+ .leaflet-sidebar-left {
+ left: 10px; } }
+
+.leaflet-sidebar-right {
+ right: 0; }
+ @media (min-width: 768px) {
+ .leaflet-sidebar-right {
+ right: 10px; } }
+
+.leaflet-sidebar-tabs {
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ background-color: #fff; }
+ .leaflet-sidebar-left .leaflet-sidebar-tabs {
+ left: 0; }
+ .leaflet-sidebar-right .leaflet-sidebar-tabs {
+ right: 0; }
+ .leaflet-sidebar-tabs, .leaflet-sidebar-tabs > ul {
+ position: absolute;
+ width: 40px;
+ margin: 0;
+ padding: 0;
+ list-style-type: none; }
+ .leaflet-sidebar-tabs > li, .leaflet-sidebar-tabs > ul > li {
+ width: 100%;
+ height: 40px;
+ color: #333;
+ font-size: 12pt;
+ overflow: hidden;
+ transition: all 80ms; }
+ .leaflet-sidebar-tabs > li:hover, .leaflet-sidebar-tabs > ul > li:hover {
+ color: #000;
+ background-color: #eee; }
+ .leaflet-sidebar-tabs > li.active, .leaflet-sidebar-tabs > ul > li.active {
+ color: #fff;
+ background-color: #0074d9; }
+ .leaflet-sidebar-tabs > li.disabled, .leaflet-sidebar-tabs > ul > li.disabled {
+ color: rgba(51, 51, 51, 0.4); }
+ .leaflet-sidebar-tabs > li.disabled:hover, .leaflet-sidebar-tabs > ul > li.disabled:hover {
+ background: transparent; }
+ .leaflet-sidebar-tabs > li.disabled > a, .leaflet-sidebar-tabs > ul > li.disabled > a {
+ cursor: default; }
+ .leaflet-sidebar-tabs > li > a, .leaflet-sidebar-tabs > ul > li > a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ line-height: 40px;
+ color: inherit;
+ text-decoration: none;
+ text-align: center;
+ cursor: pointer; }
+ .leaflet-sidebar-tabs > ul + ul {
+ bottom: 0; }
+
+.leaflet-sidebar-content {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ background-color: rgba(255, 255, 255, 0.95);
+ overflow-x: hidden;
+ overflow-y: auto; }
+ .leaflet-sidebar-left .leaflet-sidebar-content {
+ left: 40px;
+ right: 0; }
+ .leaflet-sidebar-right .leaflet-sidebar-content {
+ left: 0;
+ right: 40px; }
+ .leaflet-sidebar.collapsed > .leaflet-sidebar-content {
+ overflow-y: hidden; }
+
+.collapsed > .leaflet-sidebar-content {
+ overflow-y: hidden; }
+
+.leaflet-sidebar-pane {
+ display: none;
+ left: 0;
+ right: 0;
+ box-sizing: border-box;
+ padding: 10px 20px; }
+ .leaflet-sidebar-pane.active {
+ display: block; }
+ @media (min-width: 768px) and (max-width: 991px) {
+ .leaflet-sidebar-pane {
+ min-width: 265px; } }
+ @media (min-width: 992px) and (max-width: 1199px) {
+ .leaflet-sidebar-pane {
+ min-width: 350px; } }
+ @media (min-width: 1200px) {
+ .leaflet-sidebar-pane {
+ min-width: 420px; } }
+
+.leaflet-sidebar-header {
+ margin: -10px -20px 0;
+ height: 40px;
+ padding: 0 20px;
+ line-height: 40px;
+ font-size: 14.4pt;
+ color: #fff;
+ background-color: #0074d9; }
+ .leaflet-sidebar-right .leaflet-sidebar-header {
+ padding-left: 40px; }
+
+.leaflet-sidebar-close {
+ position: absolute;
+ top: 0;
+ width: 40px;
+ height: 40px;
+ text-align: center;
+ cursor: pointer; }
+ .leaflet-sidebar-left .leaflet-sidebar-close {
+ right: 0; }
+ .leaflet-sidebar-right .leaflet-sidebar-close {
+ left: 0; }
+
+.leaflet-sidebar {
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); }
+ @media (min-width: 768px) {
+ .leaflet-sidebar {
+ border-radius: 4px; }
+ .leaflet-sidebar.leaflet-touch {
+ border: 2px solid rgba(0, 0, 0, 0.2); } }
+
+.leaflet-sidebar-left.leaflet-touch {
+ box-shadow: none;
+ border-right: 2px solid rgba(0, 0, 0, 0.2); }
+
+@media (min-width: 768px) {
+ .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left {
+ transition: left 500ms; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left {
+ left: 315px; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left {
+ left: 400px; } }
+
+@media (min-width: 1200px) {
+ .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left {
+ left: 470px; } }
+
+.leaflet-sidebar-left.collapsed ~ .leaflet-control-container .leaflet-left {
+ left: 50px; }
+
+.leaflet-sidebar-right.leaflet-touch {
+ box-shadow: none;
+ border-left: 2px solid rgba(0, 0, 0, 0.2); }
+
+@media (min-width: 768px) {
+ .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right {
+ transition: right 500ms; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right {
+ right: 315px; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right {
+ right: 400px; } }
+
+@media (min-width: 1200px) {
+ .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right {
+ right: 470px; } }
+
+.leaflet-sidebar-right.collapsed ~ .leaflet-control-container .leaflet-right {
+ right: 50px; }
\ No newline at end of file
diff --git a/static/sidebar/sidebar.js b/static/sidebar/sidebar.js
new file mode 100644
index 0000000..f139547
--- /dev/null
+++ b/static/sidebar/sidebar.js
@@ -0,0 +1,526 @@
+// @ts-nocheck
+/**
+ * @name Sidebar
+ * @class L.Control.Sidebar
+ * @extends L.Control
+ * @param {string} id - The id of the sidebar element (without the # character)
+ * @param {Object} [options] - Optional options object
+ * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point
+ * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
+ * @param {string} [options.id] - ID of a predefined sidebar container that should be used
+ * @param {boolean} [data.close=true] Whether to add a close button to the pane header
+ * @see L.control.sidebar
+ */
+L.Control.Sidebar = L.Control.extend(/** @lends L.Control.Sidebar.prototype */ {
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
+
+ options: {
+ autopan: false,
+ closeButton: true,
+ container: null,
+ position: 'left'
+ },
+
+ /**
+ * Create a new sidebar on this object.
+ *
+ * @constructor
+ * @param {Object} [options] - Optional options object
+ * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point
+ * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
+ * @param {string} [options.container] - ID of a predefined sidebar container that should be used
+ * @param {bool} [data.close=true] Whether to add a close button to the pane header
+ */
+ initialize: function(options, deprecatedOptions) {
+ if (typeof options === 'string') {
+ console.warn('this syntax is deprecated. please use L.control.sidebar({ container }) now');
+ options = { container: options };
+ }
+
+ if (typeof options === 'object' && options.id) {
+ console.warn('this syntax is deprecated. please use L.control.sidebar({ container }) now');
+ options.container = options.id;
+ }
+
+ this._tabitems = [];
+ this._panes = [];
+ this._closeButtons = [];
+
+ L.setOptions(this, options);
+ L.setOptions(this, deprecatedOptions);
+ return this;
+ },
+
+ /**
+ * Add this sidebar to the specified map.
+ *
+ * @param {L.Map} map
+ * @returns {Sidebar}
+ */
+ onAdd: function(map) {
+ var i, child, tabContainers, newContainer, container;
+
+ // use container from previous onAdd()
+ container = this._container
+
+ // use the container given via options.
+ if (!container) {
+ container = this._container || typeof this.options.container === 'string'
+ ? L.DomUtil.get(this.options.container)
+ : this.options.container;
+ }
+
+ // if no container was specified or not found, create it and apply an ID
+ if (!container) {
+ container = L.DomUtil.create('div', 'leaflet-sidebar collapsed');
+ if (typeof this.options.container === 'string')
+ container.id = this.options.container;
+ }
+
+ // Find paneContainer in DOM & store reference
+ this._paneContainer = container.querySelector('div.leaflet-sidebar-content');
+
+ // If none is found, create it
+ if (this._paneContainer === null)
+ this._paneContainer = L.DomUtil.create('div', 'leaflet-sidebar-content', container);
+
+ // Find tabContainerTop & tabContainerBottom in DOM & store reference
+ tabContainers = container.querySelectorAll('ul.leaflet-sidebar-tabs, div.leaflet-sidebar-tabs > ul');
+ this._tabContainerTop = tabContainers[0] || null;
+ this._tabContainerBottom = tabContainers[1] || null;
+
+ // If no container was found, create it
+ if (this._tabContainerTop === null) {
+ newContainer = L.DomUtil.create('div', 'leaflet-sidebar-tabs', container);
+ newContainer.setAttribute('role', 'tablist');
+ this._tabContainerTop = L.DomUtil.create('ul', '', newContainer);
+ }
+ if (this._tabContainerBottom === null) {
+ newContainer = this._tabContainerTop.parentNode;
+ this._tabContainerBottom = L.DomUtil.create('ul', '', newContainer);
+ }
+
+ // Store Tabs in Collection for easier iteration
+ for (i = 0; i < this._tabContainerTop.children.length; i++) {
+ child = this._tabContainerTop.children[i];
+ child._sidebar = this;
+ child._id = child.querySelector('a').hash.slice(1); // FIXME: this could break for links!
+ this._tabitems.push(child);
+ }
+ for (i = 0; i < this._tabContainerBottom.children.length; i++) {
+ child = this._tabContainerBottom.children[i];
+ child._sidebar = this;
+ child._id = child.querySelector('a').hash.slice(1); // FIXME: this could break for links!
+ this._tabitems.push(child);
+ }
+
+ // Store Panes in Collection for easier iteration
+ for (i = 0; i < this._paneContainer.children.length; i++) {
+ child = this._paneContainer.children[i];
+ if (child.tagName === 'DIV' &&
+ L.DomUtil.hasClass(child, 'leaflet-sidebar-pane')) {
+ this._panes.push(child);
+
+ // Save references to close buttons
+ var closeButtons = child.querySelectorAll('.leaflet-sidebar-close');
+ if (closeButtons.length) {
+ this._closeButtons.push(closeButtons[closeButtons.length - 1]);
+ this._closeClick(closeButtons[closeButtons.length - 1], 'on');
+ }
+ }
+ }
+
+ // set click listeners for tab & close buttons
+ for (i = 0; i < this._tabitems.length; i++) {
+ this._tabClick(this._tabitems[i], 'on');
+ }
+
+ // leaflet moves the returned container to the right place in the DOM
+ return container;
+ },
+
+ /**
+ * Remove this sidebar from the map.
+ *
+ * @param {L.Map} map
+ * @returns {Sidebar}
+ */
+ onRemove: function (map) {
+ // Remove click listeners for tab & close buttons
+ for (var i = 0; i < this._tabitems.length; i++)
+ this._tabClick(this._tabitems[i], 'off');
+ for (var i = 0; i < this._closeButtons.length; i++)
+ this._closeClick(this._closeButtons[i], 'off');
+
+ this._tabitems = [];
+ this._panes = [];
+ this._closeButtons = [];
+
+ return this;
+ },
+
+ /**
+ * @method addTo(map: Map): this
+ * Adds the control to the given map. Overrides the implementation of L.Control,
+ * changing the DOM mount target from map._controlContainer.topleft to map._container
+ */
+ addTo: function (map) {
+ this.onRemove();
+ this._map = map;
+
+ this._container = this.onAdd(map);
+
+ L.DomUtil.addClass(this._container, 'leaflet-control');
+ L.DomUtil.addClass(this._container, 'leaflet-sidebar-' + this.getPosition());
+ if (L.Browser.touch)
+ L.DomUtil.addClass(this._container, 'leaflet-touch');
+
+ // when adding to the map container, we should stop event propagation
+ L.DomEvent.disableScrollPropagation(this._container);
+ L.DomEvent.disableClickPropagation(this._container);
+ L.DomEvent.on(this._container, 'contextmenu', L.DomEvent.stopPropagation);
+
+ // insert as first child of map container (important for css)
+ map._container.insertBefore(this._container, map._container.firstChild);
+
+ return this;
+ },
+
+ /**
+ * @deprecated - Please use remove() instead of removeFrom(), as of Leaflet 0.8-dev, the removeFrom() has been replaced with remove()
+ * Removes this sidebar from the map.
+ * @param {L.Map} map
+ * @returns {Sidebar}
+ */
+ removeFrom: function(map) {
+ console.warn('removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon.');
+ this._map._container.removeChild(this._container);
+ this.onRemove(map);
+
+ return this;
+ },
+
+ /**
+ * Open sidebar (if it's closed) and show the specified tab.
+ *
+ * @param {string} id - The ID of the tab to show (without the # character)
+ * @returns {L.Control.Sidebar}
+ */
+ open: function(id) {
+ var i, child, tab;
+
+ // If panel is disabled, stop right here
+ tab = this._getTab(id);
+ if (L.DomUtil.hasClass(tab, 'disabled'))
+ return this;
+
+ // Hide old active contents and show new content
+ for (i = 0; i < this._panes.length; i++) {
+ child = this._panes[i];
+ if (child.id === id)
+ L.DomUtil.addClass(child, 'active');
+ else if (L.DomUtil.hasClass(child, 'active'))
+ L.DomUtil.removeClass(child, 'active');
+ }
+
+ // Remove old active highlights and set new highlight
+ for (i = 0; i < this._tabitems.length; i++) {
+ child = this._tabitems[i];
+ if (child.querySelector('a').hash === '#' + id)
+ L.DomUtil.addClass(child, 'active');
+ else if (L.DomUtil.hasClass(child, 'active'))
+ L.DomUtil.removeClass(child, 'active');
+ }
+
+ this.fire('content', { id: id });
+
+ // Open sidebar if it's closed
+ if (L.DomUtil.hasClass(this._container, 'collapsed')) {
+ this.fire('opening');
+ L.DomUtil.removeClass(this._container, 'collapsed');
+ if (this.options.autopan) this._panMap('open');
+ }
+
+ return this;
+ },
+
+ /**
+ * Close the sidebar (if it's open).
+ *
+ * @returns {L.Control.Sidebar}
+ */
+ close: function() {
+ var i;
+
+ // Remove old active highlights
+ for (i = 0; i < this._tabitems.length; i++) {
+ var child = this._tabitems[i];
+ if (L.DomUtil.hasClass(child, 'active'))
+ L.DomUtil.removeClass(child, 'active');
+ }
+
+ // close sidebar, if it's opened
+ if (!L.DomUtil.hasClass(this._container, 'collapsed')) {
+ this.fire('closing');
+ L.DomUtil.addClass(this._container, 'collapsed');
+ if (this.options.autopan) this._panMap('close');
+ }
+
+ return this;
+ },
+
+ /**
+ * Add a panel to the sidebar
+ *
+ * @example
+ * sidebar.addPanel({
+ * id: 'userinfo',
+ * tab: '',
+ * pane: someDomNode.innerHTML,
+ * position: 'bottom'
+ * });
+ *
+ * @param {Object} [data] contains the data for the new Panel:
+ * @param {String} [data.id] the ID for the new Panel, must be unique for the whole page
+ * @param {String} [data.position='top'] where the tab will appear:
+ * on the top or the bottom of the sidebar. 'top' or 'bottom'
+ * @param {HTMLString} {DOMnode} [data.tab] content of the tab item, as HTMLstring or DOM node
+ * @param {HTMLString} {DOMnode} [data.pane] content of the panel, as HTMLstring or DOM node
+ * @param {String} [data.link] URL to an (external) link that will be opened instead of a panel
+ * @param {String} [data.title] Title for the pane header
+ * @param {String} {Function} [data.button] URL to an (external) link or a click listener function that will be opened instead of a panel
+ * @param {bool} [data.disabled] If the tab should be disabled by default
+ *
+ * @returns {L.Control.Sidebar}
+ */
+ addPanel: function(data) {
+ var pane, tab, tabHref, closeButtons, content;
+
+ // Create tab node
+ tab = L.DomUtil.create('li', data.disabled ? 'disabled' : '');
+ tabHref = L.DomUtil.create('a', '', tab);
+ tabHref.href = '#' + data.id;
+ tabHref.setAttribute('role', 'tab');
+ tabHref.innerHTML = data.tab;
+ tab._sidebar = this;
+ tab._id = data.id;
+ tab._button = data.button; // to allow links to be disabled, the href cannot be used
+ if (data.title && data.title[0] !== '<') tab.title = data.title;
+
+ // append it to the DOM and store JS references
+ if (data.position === 'bottom')
+ this._tabContainerBottom.appendChild(tab);
+ else
+ this._tabContainerTop.appendChild(tab);
+
+ this._tabitems.push(tab);
+
+ // Create pane node
+ if (data.pane) {
+ if (typeof data.pane === 'string') {
+ // pane is given as HTML string
+ pane = L.DomUtil.create('DIV', 'leaflet-sidebar-pane', this._paneContainer);
+ content = '';
+ if (data.title)
+ content += '';
+ pane.innerHTML = content + data.pane;
+ } else {
+ // pane is given as DOM object
+ pane = data.pane;
+ this._paneContainer.appendChild(pane);
+ }
+ pane.id = data.id;
+
+ this._panes.push(pane);
+
+ // Save references to close button & register click listener
+ closeButtons = pane.querySelectorAll('.leaflet-sidebar-close');
+ if (closeButtons.length) {
+ // select last button, because thats rendered on top
+ this._closeButtons.push(closeButtons[closeButtons.length - 1]);
+ this._closeClick(closeButtons[closeButtons.length - 1], 'on');
+ }
+ }
+
+ // Register click listeners, if the sidebar is on the map
+ this._tabClick(tab, 'on');
+
+ return this;
+ },
+
+ /**
+ * Removes a panel from the sidebar
+ *
+ * @example
+ * sidebar.remove('userinfo');
+ *
+ * @param {String} [id] the ID of the panel that is to be removed
+ * @returns {L.Control.Sidebar}
+ */
+ removePanel: function(id) {
+ var i, j, tab, pane, closeButtons;
+
+ // find the tab & panel by ID, remove them, and clean up
+ for (i = 0; i < this._tabitems.length; i++) {
+ if (this._tabitems[i]._id === id) {
+ tab = this._tabitems[i];
+
+ // Remove click listeners
+ this._tabClick(tab, 'off');
+
+ tab.remove();
+ this._tabitems.splice(i, 1);
+ break;
+ }
+ }
+
+ for (i = 0; i < this._panes.length; i++) {
+ if (this._panes[i].id === id) {
+ pane = this._panes[i];
+ closeButtons = pane.querySelectorAll('.leaflet-sidebar-close');
+ for (j = 0; j < closeButtons.length; j++) {
+ this._closeClick(closeButtons[j], 'off');
+ }
+
+ pane.remove();
+ this._panes.splice(i, 1);
+
+ break;
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * enables a disabled tab/panel
+ *
+ * @param {String} [id] ID of the panel to enable
+ * @returns {L.Control.Sidebar}
+ */
+ enablePanel: function(id) {
+ var tab = this._getTab(id);
+ L.DomUtil.removeClass(tab, 'disabled');
+
+ return this;
+ },
+
+ /**
+ * disables an enabled tab/panel
+ *
+ * @param {String} [id] ID of the panel to disable
+ * @returns {L.Control.Sidebar}
+ */
+ disablePanel: function(id) {
+ var tab = this._getTab(id);
+ L.DomUtil.addClass(tab, 'disabled');
+
+ return this;
+ },
+
+ onTabClick: function(e) {
+ // `this` points to the tab DOM element!
+ if (L.DomUtil.hasClass(this, 'active')) {
+ this._sidebar.close();
+ } else if (!L.DomUtil.hasClass(this, 'disabled')) {
+ if (typeof this._button === 'string') // an url
+ window.location.href = this._button;
+ else if (typeof this._button === 'function') // a clickhandler
+ this._button(e);
+ else // a normal pane
+ this._sidebar.open(this.querySelector('a').hash.slice(1));
+ }
+ },
+
+ /**
+ * (un)registers the onclick event for the given tab,
+ * depending on the second argument.
+ * @private
+ *
+ * @param {DOMelement} [tab]
+ * @param {String} [on] 'on' or 'off'
+ */
+ _tabClick: function(tab, on) {
+ var link = tab.querySelector('a');
+ if (!link.hasAttribute('href') || link.getAttribute('href')[0] !== '#')
+ return;
+
+ if (on === 'on') {
+ L.DomEvent
+ .on(tab.querySelector('a'), 'click', L.DomEvent.preventDefault, tab)
+ .on(tab.querySelector('a'), 'click', this.onTabClick, tab);
+ } else {
+ L.DomEvent.off(tab.querySelector('a'), 'click', this.onTabClick, tab);
+ }
+ },
+
+ onCloseClick: function() {
+ this.close();
+ },
+
+ /**
+ * (un)registers the onclick event for the given close button
+ * depending on the second argument
+ * @private
+ *
+ * @param {DOMelement} [closeButton]
+ * @param {String} [on] 'on' or 'off'
+ */
+ _closeClick: function(closeButton, on) {
+ if (on === 'on') {
+ L.DomEvent.on(closeButton, 'click', this.onCloseClick, this);
+ } else {
+ L.DomEvent.off(closeButton, 'click', this.onCloseClick);
+ }
+ },
+
+ /**
+ * Finds & returns the DOMelement of a tab
+ *
+ * @param {String} [id] the id of the tab
+ * @returns {DOMelement} the tab specified by id, null if not found
+ */
+ _getTab: function(id) {
+ for (var i = 0; i < this._tabitems.length; i++) {
+ if (this._tabitems[i]._id === id)
+ return this._tabitems[i];
+ }
+
+ throw Error('tab "' + id + '" not found');
+ },
+
+ /**
+ * Helper for autopan: Pans the map for open/close events
+ *
+ * @param {String} [openClose] The behaviour to enact ('open' | 'close')
+ */
+ _panMap: function(openClose) {
+ var panWidth = Number.parseInt(L.DomUtil.getStyle(this._container, 'max-width')) / 2;
+ if (
+ openClose === 'open' && this.options.position === 'left' ||
+ openClose === 'close' && this.options.position === 'right'
+ ) panWidth *= -1;
+ this._map.panBy([panWidth, 0], { duration: 0.5 });
+ }
+});
+
+/**
+ * Create a new sidebar.
+ *
+ * @example
+ * var sidebar = L.control.sidebar({ container: 'sidebar' }).addTo(map);
+ *
+ * @param {Object} [options] - Optional options object
+ * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point
+ * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right'
+ * @param {string} [options.container] - ID of a predefined sidebar container that should be used
+ * @param {boolean} [data.close=true] Whether to add a close button to the pane header
+ * @returns {Sidebar} A new sidebar instance
+ */
+L.control.sidebar = function(options, deprecated) {
+ return new L.Control.Sidebar(options, deprecated);
+};
\ No newline at end of file