diff --git a/leaflet-hash.js b/leaflet-hash.js index a90e305..039ba87 100644 --- a/leaflet-hash.js +++ b/leaflet-hash.js @@ -1,152 +1,223 @@ -(function(window) { - var HAS_HASHCHANGE = (function() { - var doc_mode = window.documentMode; - return ('onhashchange' in window) && - (doc_mode === undefined || doc_mode > 7); - })(); - - L.Hash = function(map) { - this.onHashChange = L.Util.bind(this.onHashChange, this); - - if (map) { - this.init(map); - } - }; - - L.Hash.prototype = { - map: null, - lastHash: null, - - parseHash: function(hash) { - if(hash.indexOf('#') === 0) { - hash = hash.substr(1); - } - var args = hash.split("/"); - if (args.length == 3) { - var zoom = parseInt(args[0], 10), - lat = parseFloat(args[1]), - lon = parseFloat(args[2]); - if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { - return false; - } else { - return { - center: new L.LatLng(lat, lon), - zoom: zoom - }; - } - } else { - return false; - } - }, - - formatHash: function(map) { - var center = map.getCenter(), - zoom = map.getZoom(), - precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - - return "#" + [zoom, - center.lat.toFixed(precision), - center.lng.toFixed(precision) - ].join("/"); - }, - - init: function(map) { - this.map = map; - - this.map.on("moveend", this.onMapMove, this); - - // reset the hash - this.lastHash = null; - this.onHashChange(); - - if (!this.isListening) { - this.startListening(); - } - }, - - remove: function() { - this.map = null; - if (this.isListening) { - this.stopListening(); - } - }, - - onMapMove: function(map) { - // bail if we're moving the map (updating from a hash), - // or if the map is not yet loaded - - if (this.movingMap || !this.map._loaded) { - return false; - } - - var hash = this.formatHash(this.map); - if (this.lastHash != hash) { - location.replace(hash); - this.lastHash = hash; - } - }, - - movingMap: false, - update: function() { - var hash = location.hash; - if (hash === this.lastHash) { - return; - } - var parsed = this.parseHash(hash); - if (parsed) { - this.movingMap = true; - - this.map.setView(parsed.center, parsed.zoom); - - this.movingMap = false; - } else { - this.onMapMove(this.map); - } - }, - - // defer hash change updates every 100ms - changeDefer: 100, - changeTimeout: null, - onHashChange: function() { - // throttle calls to update() so that they only happen every - // `changeDefer` ms - if (!this.changeTimeout) { - var that = this; - this.changeTimeout = setTimeout(function() { - that.update(); - that.changeTimeout = null; - }, this.changeDefer); - } - }, - - isListening: false, - hashChangeInterval: null, - startListening: function() { - if (HAS_HASHCHANGE) { - L.DomEvent.addListener(window, "hashchange", this.onHashChange); - } else { - clearInterval(this.hashChangeInterval); - this.hashChangeInterval = setInterval(this.onHashChange, 50); - } - this.isListening = true; - }, - - stopListening: function() { - if (HAS_HASHCHANGE) { - L.DomEvent.removeListener(window, "hashchange", this.onHashChange); - } else { - clearInterval(this.hashChangeInterval); - } - this.isListening = false; - } - }; - L.hash = function(map) { - return new L.Hash(map); - }; - L.Map.prototype.addHash = function() { - this._hash = L.hash(this); - }; - L.Map.prototype.removeHash = function() { - this._hash.remove(); - }; -})(window); +(function() { + + L.Hash = L.Class.extend({ + initialize: function(map, options) { + this.map = map; + this.options = options || {}; + if (!this.options.path) { + if (this.options.lc) { + this.options.path = '{base}/{z}/{lat}/{lng}'; + } else { + this.options.path = '{z}/{lat}/{lng}'; + } + } + if (this.options.lc && !this.options.formatBase) { + this.options.formatBase = [ + /[\s\:A-Z]/g, function(match) { + if (match.match(/\s/)) { + return "_"; + } else if (match.match(/\:/)) { + return ""; + } + if (match.match(/[A-Z]/)) { + return match.toLowerCase(); + } + } + ]; + }if (location.hash) { + this.updateFromState(this.parseHash(location.hash)); + } + if (this.map._loaded) { + return this.startListning(); + } else { + return this.map.on("load", this.startListning,this); + } + }, + startListning: function() { + var onHashChange, + _this = this; + if (location.hash) { + this.updateFromState(this.parseHash(location.hash)); + } + if (history.pushState) { + if (!location.hash) { + history.replaceState.apply(history, this.formatState()); + } + window.onpopstate = function(event) { + + if (event.state) { + return _this.updateFromState(event.state); + } + }; + this.map.on("moveend", function() { + var pstate; + pstate = _this.formatState(); + if (location.hash !== pstate[2] && !_this.moving) { + return history.pushState.apply(history, pstate); + } + }); + } else { + if (!location.hash) { + location.hash = this.formatState()[2]; + } + onHashChange = function() { + + var pstate; + pstate = _this.formatState(); + if (location.hash !== pstate[2] && !_this.moving) { + return location.hash = pstate[2]; + } + }; + this.map.on("moveend", onHashChange); + if (('onhashchange' in window) && (window.documentMode === void 0 || window.documentMode > 7)) { + window.onhashchange = function() { + if (location.hash) { + + return _this.updateFromState(_this.parseHash(location.hash)); + } + }; + } else { + this.hashChangeInterval = setInterval(onHashChange, 50); + } + } + return this.map.on("baselayerchange", function(e) { + var pstate, _ref; + _this.base = (_ref = _this.options.lc._layers[e.layer._leaflet_id].name).replace.apply(_ref, _this.options.formatBase); + pstate = _this.formatState(); + if (history.pushState) { + if (location.hash !== pstate[2] && !_this.moving) { + return history.pushState.apply(history, pstate); + } + } else { + if (location.hash !== pstate[2] && !_this.moving) { + return location.hash = pstate[2]; + } + } + }); + }, + parseHash: function(hash) { + + var args, lat, latIndex, lngIndex, lon, out, path, zIndex, zoom; + path = this.options.path.split("/"); + zIndex = path.indexOf("{z}"); + latIndex = path.indexOf("{lat}"); + lngIndex = path.indexOf("{lng}"); + if (hash.indexOf("#") === 0) { + hash = hash.substr(1); + } + args = hash.split("/"); + + if (args.length > 2) { + zoom = parseInt(args[zIndex], 10); + lat = parseFloat(args[latIndex]); + lon = parseFloat(args[lngIndex]); + if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { + return false; + } else { + out = { + center: new L.LatLng(lat, lon), + zoom: zoom + }; + if (args.length > 3) { + out.base = args[path.indexOf("{base}")]; + return out; + } else { + return out; + } + } + } else { + + return false; + } + }, + updateFromState: function(state) { + if (this.moving || !state) { + return; + } + this.moving = true; + this.map.setView(state.center, state.zoom); + if (state.base) { + this.setBase(state.base); + } + this.moving = false; + return true; + }, + formatState: function() { + var center, precision, state, template, zoom; + center = this.map.getCenter(); + zoom = this.map.getZoom(); + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + state = { + center: center, + zoom: zoom + }; + template = { + lat: center.lat.toFixed(precision), + lng: center.lng.toFixed(precision), + z: zoom + }; + if (this.options.path.indexOf("{base}") > -1) { + state.base = this.getBase(); + template.base = state.base; + } + return [state, "a", '#' + L.Util.template(this.options.path, template)]; + }, + setBase: function(base) { + var i, inputs, len, _ref; + this.base = base; + inputs = this.options.lc._form.getElementsByTagName('input'); + len = inputs.length; + i = 0; + while (i < len) { + if (inputs[i].name === 'leaflet-base-layers' && (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase) === base) { + inputs[i].checked = true; + this.options.lc._onInputClick(); + return true; + } + i++; + } + }, + getBase: function() { + var i, inputs, len, _ref; + if (this.base) { + return this.base; + } + inputs = this.options.lc._form.getElementsByTagName('input'); + len = inputs.length; + i = 0; + while (i < len) { + if (inputs[i].name === 'leaflet-base-layers' && inputs[i].checked) { + this.base = (_ref = this.options.lc._layers[inputs[i].layerId].name).replace.apply(_ref, this.options.formatBase); + return this.base; + } + } + return false; + }, + remove: function() { + this.map.off("moveend"); + if (window.onpopstate) { + window.onpopstate = null; + } + location.hash = ""; + return clearInterval(this.hashChangeInterval); + } + }); + + L.hash = function(map, options) { + return new L.Hash(map, options); + }; + + L.Map.include({ + addHash: function(options) { + + this._hash = L.hash(this, options); + + return this; + }, + removeHash: function() { + this._hash.remove(); + return this; + } + }); + +}).call(this); \ No newline at end of file diff --git a/package.json b/package.json index a3f70fa..fa1bc4e 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,50 @@ { - "author": "Michael Evans", - "name": "leaflet-hash", - "description": "linkable location hashes for leaflet", - "version": "0.0.0", - "homepage": "https://github.com/mlevans/leaflet-hash", - "repository": { - "type": "git", - "url": "https://github.com/mlevans/leaflet-hash.git" - }, - "devDependencies": { - "mocha": "~1.8", + "name": "leaflet-hash", + "version": "0.2.0", + "description": "Add url hashes to leaflet", + "main": "leaflet-hash.js", + "repository": { + "type": "git", + "url": "https://github.com/mlevans/leaflet-hash" + }, + "keywords": [ + "leaflet", + "maping", + "hash" + ], + "author": { + "name" : "Michael Lawrence Evans", + "email" : "michael@codeforamerica.org", + "url" : "http://www.mlevans.com/" + }, + "contributors": [{ + "name" : "Calvin Metcalf", + "email" : "calvin.metcalf@state.ma.us", + "url" : "http://cwmma.tumblr.com/" + }, + { + "name" : "Bobby Sudekum", + "url" : "http://www.visuallybs.com/" + }, + { + "name" : "Yohan Boniface", + "url" : "http://yohanboniface.me/" + }, + { + "name" : "Bryan Buchs", + "email" : "bryan@bryanbuchs.com", + "url" : "http://www.bryanbuchs.com/" + }, + { + "name" : "Tom MacWright", + "email" : "tom@macwright.org", + "url" : "http://macwright.org" + } + + ], + "license": "MIT", + "dependencies": { + "mocha": "~1.8", "expect.js": "~0.2.0" - }, - "optionalDependencies": {}, - "engines": { - "node": "*" - } + } } diff --git a/test/spec/hash.js b/test/spec/hash.js index 371c2e2..938cd13 100644 --- a/test/spec/hash.js +++ b/test/spec/hash.js @@ -6,17 +6,27 @@ describe("L.Hash", function() { '_leaflet_hashchange16', '_leaflet_hashchange17', '_leaflet_hashchange18']); beforeEach(function() { + location.hash = ""; map = new L.Map(document.createElement('div')); }); it('sets a hash when the map is moved', function() { + var hash = L.hash(map); map.setView([51.505, -0.09], 13); expect(location.hash).to.be('#13/51.5050/-0.0900'); }); - + + it('sets a hash in the other manner', function() { + map.addHash(); + map.setView([51.505, -0.09], 13); + expect(location.hash).to.be('#13/51.5050/-0.0900'); + }); + it('uses a hash set initially on the page', function(done) { + location.hash = '#13/10/40'; + var hash = L.hash(map); window.setTimeout(function() { expect(Math.round(map.getCenter().lat)).to.be(10); @@ -38,13 +48,53 @@ describe("L.Hash", function() { it('does not acknowledge a junk hash', function(done) { var hash = L.hash(map); - map.setView([51, 2], 13); location.hash = '#foo'; + map.setView([51, 2], 13); + window.setTimeout(function() { expect(Math.round(map.getCenter().lat)).to.eql(51); expect(Math.round(map.getCenter().lng)).to.eql(2); done(); }, 200); }); - + + it('sets a hash when the layer changes', function() { + var de = L.tileLayer('http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png').addTo(map); + var hash = L.hash(map,{lc:L.control.layers({"A Layer":de}).addTo(map)}); + map.setView([51.505, -0.09], 13); + expect(location.hash).to.be('#a_layer/13/51.5050/-0.0900'); + }); + it('uses a hash set initially on the page', function(done) { + location.hash = '#a_layer/13/10/40'; + + var lc = L.control.layers({"A Layer": + L.tileLayer('http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png') + }).addTo(map); + var hash = L.hash(map,{lc:lc}); + window.setTimeout(function() { + expect(Math.round(map.getCenter().lat)).to.be(10); + expect(Math.round(map.getCenter().lng)).to.be(40); + //test which layer is set? + done(); + }, 200); + }); + it('modify the hash options', function() { + var de = L.tileLayer('http://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png').addTo(map); + var hash = L.hash(map,{ + path : '{z}/{lat}/{lng}/{base}', + lc : L.control.layers({"A Layer":de}).addTo(map), + formatBase: [ + /[\s?a-z]/g, function(match) { + if (match.match(/\s/)) { + return "-"; + } + if (match.match(/[a-z]/)) { + return match.toUpperCase(); + } + } + ] + }); + map.setView([51.505, -0.09], 13); + expect(location.hash).to.be('#13/51.5050/-0.0900/A-LAYER'); + }); });