diff --git a/.gitignore b/.gitignore index e43b0f9..767381a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .DS_Store +~* +.c9revisions +node_modules \ No newline at end of file diff --git a/Cakefile b/Cakefile new file mode 100644 index 0000000..0dfa2ac --- /dev/null +++ b/Cakefile @@ -0,0 +1,24 @@ +fs = require 'fs' +coffee = require 'coffee-script' +uglifyjs = require 'uglify-js' + +task 'build', 'build it', () -> + fs.readFile './src/leaflet.hash.coffee', 'utf8', (e,d)-> + unless e + fs.writeFile './dist/leaflet.hash.js', coffee.compile d + console.log "compliled" + +task 'min', 'build it small', () -> + fs.readFile './src/leaflet.hash.coffee', 'utf8', (e,d)-> + unless e + j = coffee.compile d + ast = uglifyjs.parse j + ast.figure_out_scope(); + ast.compute_char_frequency(); + ast.mangle_names(); + fs.writeFile './dist/leaflet.hash.min.js', ast.print_to_string() + console.log "minified" + +task 'all', 'build it all', () -> + invoke 'build' + invoke 'min' \ No newline at end of file diff --git a/README.md b/README.md index 5156f96..2e4d5cb 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,24 @@ You can view a demo of leaflet-hash at [mlevans.github.com/leaflet-hash/map.html ```javascript // Assuming your map instance is in a variable called map - var hash = new L.Hash(map); + map.addHash(); ``` +4. optionally you can pass a an object with a few options + * lc: pass an instance of L.Control.Layer, the baselayers will be put in the hash + * path: template for the url hash, defaults to '{z}/{lat}/{lng}' or '{base}/{z}/{lat}/{lng}' if lc is specified, parts need to be seperated by "/" + * formatBase: an array of length 2 that will be used as the arguments of the overlay names before they go into the url hash, the default turns whitespace to underscores and all lowercase just pass "[//]" if you want it unchanged + +### Hacking Around + +1. Build with cake, + + ```bash + npm install #install dependencies + cake build #builds it regular + cake min #builds minified + cake all #does both + ``` +2. leaflet-hash.coffee is the file to modify. ### Author [@mlevans](http://github.com/mlevans) diff --git a/dist/leaflet.hash.js b/dist/leaflet.hash.js new file mode 100644 index 0000000..50fe756 --- /dev/null +++ b/dist/leaflet.hash.js @@ -0,0 +1,264 @@ +(function() { + var Hash, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __slice = [].slice; + + Hash = (function() { + + function Hash(map, options) { + this.map = map; + this.options = options != null ? options : {}; + this.remove = __bind(this.remove, this); + + this.getBase = __bind(this.getBase, this); + + this.setBase = __bind(this.setBase, this); + + this.formatState = __bind(this.formatState, this); + + this.updateFromState = __bind(this.updateFromState, this); + + this.startListning = __bind(this.startListning, this); + + 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 = [ + /[\sA-Z]/g, function(match) { + if (match.match(/\s/)) { + return "_ "; + } + if (match.match(/[A-Z]/)) { + return match.toLowerCase(); + } + } + ]; + } + if (this.map._loaded) { + this.startListning(); + } else { + this.map.on("load", this.startListning); + } + } + + Hash.prototype.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); + } + } + if (this.options.lc) { + 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]; + } + } + }); + } + }; + + Hash.prototype.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; + } + }; + + Hash.prototype.updateFromState = function(state) { + if (this.moving) { + return; + } + this.moving = true; + this.map.setView(state.center, state.zoom); + if (state.base) { + this.setBase(state.base); + } + this.moving = false; + return true; + }; + + Hash.prototype.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)]; + }; + + Hash.prototype.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++; + } + }; + + Hash.prototype.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; + }; + + Hash.prototype.remove = function() { + this.map.off("moveend"); + if (window.onpopstate) { + window.onpopstate = null; + } + location.hash = ""; + return clearInterval(this.hashChangeInterval); + }; + + return Hash; + + })(); + + L.Hash = Hash; + + L.hash = function() { + var params; + params = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return (function(func, args, ctor) { + ctor.prototype = func.prototype; + var child = new ctor, result = func.apply(child, args); + return Object(result) === result ? result : child; + })(L.Hash, params, function(){}); + }; + + L.Map.include({ + addHash: function() { + var params, + _this = this; + params = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + if (this._loaded) { + this._hash = (function(func, args, ctor) { + ctor.prototype = func.prototype; + var child = new ctor, result = func.apply(child, args); + return Object(result) === result ? result : child; + })(Hash, [this].concat(__slice.call(params)), function(){}); + } else { + this.on("load", function() { + return _this._hash = (function(func, args, ctor) { + ctor.prototype = func.prototype; + var child = new ctor, result = func.apply(child, args); + return Object(result) === result ? result : child; + })(Hash, [_this].concat(__slice.call(params)), function(){}); + }); + } + return this; + }, + removeHash: function() { + this._hash.remove(); + return this; + } + }); + +}).call(this); diff --git a/dist/leaflet.hash.min.js b/dist/leaflet.hash.min.js new file mode 100644 index 0000000..324d8cd --- /dev/null +++ b/dist/leaflet.hash.min.js @@ -0,0 +1 @@ +(function(){var t,e=function(t,e){return function(){return t.apply(e,arguments)}},a=[].slice;t=function(){function t(t,a){this.map=t;this.options=a!=null?a:{};this.remove=e(this.remove,this);this.getBase=e(this.getBase,this);this.setBase=e(this.setBase,this);this.formatState=e(this.formatState,this);this.updateFromState=e(this.updateFromState,this);this.startListning=e(this.startListning,this);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=[/[\sA-Z]/g,function(t){if(t.match(/\s/)){return"_ "}if(t.match(/[A-Z]/)){return t.toLowerCase()}}]}if(this.map._loaded){this.startListning()}else{this.map.on("load",this.startListning)}}t.prototype.startListning=function(){var t,e=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(t){if(t.state){return e.updateFromState(t.state)}};this.map.on("moveend",function(){var t;t=e.formatState();if(location.hash!==t[2]&&!e.moving){return history.pushState.apply(history,t)}})}else{if(!location.hash){location.hash=this.formatState()[2]}t=function(){var t;t=e.formatState();if(location.hash!==t[2]&&!e.moving){return location.hash=t[2]}};this.map.on("moveend",t);if("onhashchange"in window&&(window.documentMode===void 0||window.documentMode>7)){window.onhashchange=function(){if(location.hash){return e.updateFromState(e.parseHash(location.hash))}}}else{this.hashChangeInterval=setInterval(t,50)}}if(this.options.lc){return this.map.on("baselayerchange",function(t){var a,s;e.base=(s=e.options.lc._layers[t.layer._leaflet_id].name).replace.apply(s,e.options.formatBase);a=e.formatState();if(history.pushState){if(location.hash!==a[2]&&!e.moving){return history.pushState.apply(history,a)}}else{if(location.hash!==a[2]&&!e.moving){return location.hash=a[2]}}})}};t.prototype.parseHash=function(t){var e,a,s,i,n,o,r,h,l;r=this.options.path.split("/");h=r.indexOf("{z}");s=r.indexOf("{lat}");i=r.indexOf("{lng}");if(t.indexOf("#")===0){t=t.substr(1)}e=t.split("/");if(e.length>2){l=parseInt(e[h],10);a=parseFloat(e[s]);n=parseFloat(e[i]);if(isNaN(l)||isNaN(a)||isNaN(n)){return false}else{o={center:new L.LatLng(a,n),zoom:l};if(e.length>3){o.base=e[r.indexOf("{base}")];return o}else{return o}}}else{return false}};t.prototype.updateFromState=function(t){if(this.moving){return}this.moving=true;this.map.setView(t.center,t.zoom);if(t.base){this.setBase(t.base)}this.moving=false;return true};t.prototype.formatState=function(){var t,e,a,s,i;t=this.map.getCenter();i=this.map.getZoom();e=Math.max(0,Math.ceil(Math.log(i)/Math.LN2));a={center:t,zoom:i};s={lat:t.lat.toFixed(e),lng:t.lng.toFixed(e),z:i};if(this.options.path.indexOf("{base}")>-1){a.base=this.getBase();s.base=a.base}return[a,"a","#"+L.Util.template(this.options.path,s)]};t.prototype.setBase=function(t){var e,a,s,i;this.base=t;a=this.options.lc._form.getElementsByTagName("input");s=a.length;e=0;while(e 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 has no zoom set - - if (this.movingMap || this.map.getZoom() === 0) { - 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) { - // console.info("(no change)"); - return; - } - var parsed = this.parseHash(hash); - if (parsed) { - // console.log("parsed:", parsed.zoom, parsed.center.toString()); - this.movingMap = true; - - this.map.setView(parsed.center, parsed.zoom); - - this.movingMap = false; - } else { - // console.warn("parse error; resetting:", this.map.getCenter(), this.map.getZoom()); - 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); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9503ef0 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "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/" + } + + ], + "license": "MIT", + "dependencies": { + "coffee-script":"1.x", + "uglify-js" : ">= 0.0.1" + } +} diff --git a/src/leaflet.hash.coffee b/src/leaflet.hash.coffee new file mode 100644 index 0000000..acf7b24 --- /dev/null +++ b/src/leaflet.hash.coffee @@ -0,0 +1,142 @@ +class Hash + constructor: (@map,@options={}) -> + unless @options.path + if @options.lc + @options.path = '{base}/{z}/{lat}/{lng}' + else + @options.path = '{z}/{lat}/{lng}' + if @options.lc and not @options.formatBase + @options.formatBase = [ + /[\sA-Z]/g + (match)-> + return "_ " if match.match /\s/ + return match.toLowerCase() if match.match /[A-Z]/ + ] + if @map._loaded + @startListning() + else + @map.on "load", @startListning + startListning : => + @updateFromState @parseHash(location.hash) if location.hash + if history.pushState + history.replaceState(@formatState()...) unless location.hash + window.onpopstate=(event)=> + @updateFromState(event.state) if event.state + @map.on "moveend", ()=> + pstate = @formatState() + if location.hash != pstate[2] and !@moving + history.pushState pstate... + else + location.hash = @formatState()[2] unless location.hash + onHashChange = ()=> + pstate = @formatState() + if location.hash != pstate[2] and !@moving + location.hash = pstate[2] + @map.on "moveend", onHashChange + if ('onhashchange' of window) and (window.documentMode == undefined or window.documentMode > 7) + window.onhashchange = ()=> + @updateFromState @parseHash(location.hash) if location.hash + else + @hashChangeInterval = setInterval onHashChange, 50 + if @options.lc + @map.on "baselayerchange", (e)=> + @base = @options.lc._layers[e.layer._leaflet_id].name.replace(@options.formatBase...) + pstate = @formatState() + if history.pushState + if location.hash != pstate[2] and !@moving + history.pushState pstate... + else + if location.hash != pstate[2] and !@moving + location.hash = pstate[2] + + parseHash : (hash) -> + path = @options.path.split("/") + zIndex = path.indexOf("{z}") + latIndex = path.indexOf("{lat}") + lngIndex = path.indexOf("{lng}") + hash = hash.substr(1) if hash.indexOf("#") is 0 + args = hash.split("/") + if args.length > 2 + zoom = parseInt(args[zIndex], 10) + lat = parseFloat(args[latIndex]) + lon = parseFloat(args[lngIndex]) + if isNaN(zoom) or isNaN(lat) or isNaN(lon) + return false + else + out ={ + center: new L.LatLng(lat, lon) + zoom: zoom + } + if args.length > 3 + out.base = args[path.indexOf("{base}")] + out + else + out + else + false + updateFromState : (state)=> + return if @moving + @moving = true + @map.setView state.center, state.zoom + @setBase state.base if state.base + @moving = false + true + formatState : () => + center = @map.getCenter() + zoom = @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 @options.path.indexOf("{base}") > -1 + state.base = @getBase() + template.base = state.base + [ + state + "a" + '#'+L.Util.template @options.path,template + ] + setBase : (base)=> + @base = base + inputs = @options.lc._form.getElementsByTagName('input') + len = inputs.length + i = 0 + while i + if @base + return @base + inputs = @options.lc._form.getElementsByTagName('input') + len = inputs.length + i = 0 + while i + @map.off "moveend" + if window.onpopstate + window.onpopstate = null + location.hash="" + clearInterval @hashChangeInterval + +L.Hash = Hash + +L.hash = (params...)-> + return new L.Hash(params...) + +L.Map.include + addHash:(params...)-> + if @_loaded + @_hash = new Hash(@,params...) + else + @on "load",()=> + @_hash = new Hash(@,params...) + @ + removeHash:()-> + @_hash.remove() + @ \ No newline at end of file