stamen-modestmaps-js-52d7710/0000775000175000017500000000000011661034367015011 5ustar daviddavidstamen-modestmaps-js-52d7710/modestmaps.js0000664000175000017500000023635511653546654017551 0ustar daviddavid/*! * Modest Maps JS v0.21.0 * http://modestmaps.com/ * * Copyright (c) 2011 Stamen Design, All Rights Reserved. * * Open source under the BSD License. * http://creativecommons.org/licenses/BSD/ * * Versioned using Semantic Versioning (v.major.minor.patch) * See CHANGELOG and http://semver.org/ for more details. * */ // namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = {}; } } (function(MM) { // Make inheritance bearable: clone one level of properties MM.extend = function(child, parent) { for (var property in parent.prototype) { if (typeof child.prototype[property] == "undefined") { child.prototype[property] = parent.prototype[property]; } } return child; }; MM.getFrame = function () { // native animation frames // http://webstuff.nfshost.com/anim-timing/Overview.html // http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // can't apply these directly to MM because Chrome needs window // to own webkitRequestAnimationFrame (for example) // perhaps we should namespace an alias onto window instead? // e.g. window.mmRequestAnimationFrame? return function(callback) { (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(function () { callback(+new Date()); }, 10); })(callback); }; }(); // Inspired by LeafletJS MM.transformProperty = (function(props) { if (!this.document) return; // node.js safety var style = document.documentElement.style; for (var i = 0; i < props.length; i++) { if (props[i] in style) { return props[i]; } } return false; })(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); MM.matrixString = function(point) { // Make the result of point.scale * point.width a whole number. if (point.scale * point.width % 1) { point.scale += (1 - point.scale * point.width % 1) / point.width; } if (MM._browser.webkit3d) { return 'matrix3d(' + [(point.scale || '1'), '0,0,0,0', (point.scale || '1'), '0,0', '0,0,1,0', (point.x + (((point.width * point.scale) - point.width) / 2)).toFixed(4), (point.y + (((point.height * point.scale) - point.height) / 2)).toFixed(4), 0,1].join(',') + ')'; } else { var unit = (MM.transformProperty == 'MozTransform') ? 'px' : ''; return 'matrix(' + [(point.scale || '1'), 0, 0, (point.scale || '1'), (point.x + (((point.width * point.scale) - point.width) / 2)) + unit, (point.y + (((point.height * point.scale) - point.height) / 2)) + unit ].join(',') + ')'; } }; MM._browser = (function(window) { return { webkit: ('WebKitCSSMatrix' in window), webkit3d: ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()) }; })(this); // use this for node.js global MM.moveElement = function(el, point) { if (MM.transformProperty) { // Optimize for identity transforms, where you don't actually // need to change this element's string. Browsers can optimize for // the .style.left case but not for this CSS case. var ms = MM.matrixString(point); if (el[MM.transformProperty] !== ms) { el.style[MM.transformProperty] = el[MM.transformProperty] = ms; } } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; el.style.width = Math.ceil(point.width * point.scale) + 'px'; el.style.height = Math.ceil(point.height * point.scale) + 'px'; } }; // Events // Cancel an event: prevent it from bubbling MM.cancelEvent = function(e) { // there's more than one way to skin this cat e.cancelBubble = true; e.cancel = true; e.returnValue = false; if (e.stopPropagation) { e.stopPropagation(); } if (e.preventDefault) { e.preventDefault(); } return false; }; // see http://ejohn.org/apps/jselect/event.html for the originals MM.addEvent = function(obj, type, fn) { if (obj.attachEvent) { obj['e'+type+fn] = fn; obj[type+fn] = function(){ obj['e'+type+fn](window.event); }; obj.attachEvent('on'+type, obj[type+fn]); } else { obj.addEventListener(type, fn, false); if (type == 'mousewheel') { obj.addEventListener('DOMMouseScroll', fn, false); } } }; // From underscore.js MM.bind = function(func, obj) { var slice = Array.prototype.slice; var nativeBind = Function.prototype.bind; if (func.bind === nativeBind && nativeBind) { return nativeBind.apply(func, slice.call(arguments, 1)); } var args = slice.call(arguments, 2); return function() { return func.apply(obj, args.concat(slice.call(arguments))); }; }; MM.removeEvent = function( obj, type, fn ) { if ( obj.detachEvent ) { obj.detachEvent('on'+type, obj[type+fn]); obj[type+fn] = null; } else { obj.removeEventListener(type, fn, false); if (type == 'mousewheel') { obj.removeEventListener('DOMMouseScroll', fn, false); } } }; // Cross-browser function to get current element style property MM.getStyle = function(el,styleProp) { if (el.currentStyle) return el.currentStyle[styleProp]; else if (window.getComputedStyle) return document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); }; // Point MM.Point = function(x, y) { this.x = parseFloat(x); this.y = parseFloat(y); }; MM.Point.prototype = { x: 0, y: 0, toString: function() { return "(" + this.x.toFixed(3) + ", " + this.y.toFixed(3) + ")"; } }; // Get the euclidean distance between two points MM.Point.distance = function(p1, p2) { var dx = (p2.x - p1.x); var dy = (p2.y - p1.y); return Math.sqrt(dx*dx + dy*dy); }; // Get a point between two other points, biased by `t`. MM.Point.interpolate = function(p1, p2, t) { var px = p1.x + (p2.x - p1.x) * t; var py = p1.y + (p2.y - p1.y) * t; return new MM.Point(px, py); }; // Coordinate // ---------- // An object representing a tile position, at as specified zoom level. // This is not necessarily a precise tile - `row`, `column`, and // `zoom` can be floating-point numbers, and the `container()` function // can be used to find the actual tile that contains the point. MM.Coordinate = function(row, column, zoom) { this.row = row; this.column = column; this.zoom = zoom; }; MM.Coordinate.prototype = { row: 0, column: 0, zoom: 0, toString: function() { return "(" + this.row.toFixed(3) + ", " + this.column.toFixed(3) + " @" + this.zoom.toFixed(3) + ")"; }, // Quickly generate a string representation of this coordinate to // index it in hashes. toKey: function() { // We've tried to use efficient hash functions here before but we took // them out. Contributions welcome but watch out for collisions when the // row or column are negative and check thoroughly (exhaustively) before // committing. return [ this.zoom, this.row, this.column ].join(','); }, // Clone this object. copy: function() { return new MM.Coordinate(this.row, this.column, this.zoom); }, // Get the actual, rounded-number tile that contains this point. container: function() { // using floor here (not parseInt, ~~) because we want -0.56 --> -1 return new MM.Coordinate(Math.floor(this.row), Math.floor(this.column), Math.floor(this.zoom)); }, // Recalculate this Coordinate at a different zoom level and return the // new object. zoomTo: function(destination) { var power = Math.pow(2, destination - this.zoom); return new MM.Coordinate(this.row * power, this.column * power, destination); }, // Recalculate this Coordinate at a different relative zoom level and return the // new object. zoomBy: function(distance) { var power = Math.pow(2, distance); return new MM.Coordinate(this.row * power, this.column * power, this.zoom + distance); }, // Move this coordinate up by `dist` coordinates up: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row - dist, this.column, this.zoom); }, // Move this coordinate right by `dist` coordinates right: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row, this.column + dist, this.zoom); }, // Move this coordinate down by `dist` coordinates down: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row + dist, this.column, this.zoom); }, // Move this coordinate left by `dist` coordinates left: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row, this.column - dist, this.zoom); } }; // Location // -------- MM.Location = function(lat, lon) { this.lat = parseFloat(lat); this.lon = parseFloat(lon); }; MM.Location.prototype = { lat: 0, lon: 0, toString: function() { return "(" + this.lat.toFixed(3) + ", " + this.lon.toFixed(3) + ")"; } }; // returns approximate distance between start and end locations // // default unit is meters // // you can specify different units by optionally providing the // earth's radius in the units you desire // // Default is 6,378,000 metres, suggested values are: // // * 3963.1 statute miles // * 3443.9 nautical miles // * 6378 km // // see [Formula and code for calculating distance based on two lat/lon locations](http://jan.ucc.nau.edu/~cvm/latlon_formula.html) MM.Location.distance = function(l1, l2, r) { if (!r) { // default to meters r = 6378000; } var deg2rad = Math.PI / 180.0, a1 = l1.lat * deg2rad, b1 = l1.lon * deg2rad, a2 = l2.lat * deg2rad, b2 = l2.lon * deg2rad, c = Math.cos(a1) * Math.cos(b1) * Math.cos(a2) * Math.cos(b2), d = Math.cos(a1) * Math.sin(b1) * Math.cos(a2) * Math.sin(b2), e = Math.sin(a1) * Math.sin(a2); return Math.acos(c + d + e) * r; }; // Interpolates along a great circle, f between 0 and 1 // // * FIXME: could be heavily optimized (lots of trig calls to cache) // * FIXME: could be inmproved for calculating a full path MM.Location.interpolate = function(l1, l2, f) { if (l1.lat === l2.lat && l1.lon === l2.lon) { return new MM.Location(l1.lat, l1.lon); } var deg2rad = Math.PI / 180.0, lat1 = l1.lat * deg2rad, lon1 = l1.lon * deg2rad, lat2 = l2.lat * deg2rad, lon2 = l2.lon * deg2rad; var d = 2 * Math.asin( Math.sqrt( Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lon1 - lon2) / 2), 2))); var bearing = Math.atan2( Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2) ) / -(Math.PI / 180); bearing = bearing < 0 ? 360 + bearing : bearing; var A = Math.sin((1-f)*d)/Math.sin(d); var B = Math.sin(f*d)/Math.sin(d); var x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2); var y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2); var z = A * Math.sin(lat1) + B * Math.sin(lat2); var latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); var lonN = Math.atan2(y,x); return new MM.Location(latN / deg2rad, lonN / deg2rad); }; // Transformation // -------------- MM.Transformation = function(ax, bx, cx, ay, by, cy) { this.ax = ax; this.bx = bx; this.cx = cx; this.ay = ay; this.by = by; this.cy = cy; }; MM.Transformation.prototype = { ax: 0, bx: 0, cx: 0, ay: 0, by: 0, cy: 0, transform: function(point) { return new MM.Point(this.ax * point.x + this.bx * point.y + this.cx, this.ay * point.x + this.by * point.y + this.cy); }, untransform: function(point) { return new MM.Point((point.x * this.by - point.y * this.bx - this.cx * this.by + this.cy * this.bx) / (this.ax * this.by - this.ay * this.bx), (point.x * this.ay - point.y * this.ax - this.cx * this.ay + this.cy * this.ax) / (this.bx * this.ay - this.by * this.ax)); } }; // Generates a transform based on three pairs of points, // a1 -> a2, b1 -> b2, c1 -> c2. MM.deriveTransformation = function(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y, c1x, c1y, c2x, c2y) { var x = MM.linearSolution(a1x, a1y, a2x, b1x, b1y, b2x, c1x, c1y, c2x); var y = MM.linearSolution(a1x, a1y, a2y, b1x, b1y, b2y, c1x, c1y, c2y); return new MM.Transformation(x[0], x[1], x[2], y[0], y[1], y[2]); }; // Solves a system of linear equations. // // t1 = (a * r1) + (b + s1) + c // t2 = (a * r2) + (b + s2) + c // t3 = (a * r3) + (b + s3) + c // // r1 - t3 are the known values. // a, b, c are the unknowns to be solved. // returns the a, b, c coefficients. MM.linearSolution = function(r1, s1, t1, r2, s2, t2, r3, s3, t3) { // make them all floats r1 = parseFloat(r1); s1 = parseFloat(s1); t1 = parseFloat(t1); r2 = parseFloat(r2); s2 = parseFloat(s2); t2 = parseFloat(t2); r3 = parseFloat(r3); s3 = parseFloat(s3); t3 = parseFloat(t3); var a = (((t2 - t3) * (s1 - s2)) - ((t1 - t2) * (s2 - s3))) / (((r2 - r3) * (s1 - s2)) - ((r1 - r2) * (s2 - s3))); var b = (((t2 - t3) * (r1 - r2)) - ((t1 - t2) * (r2 - r3))) / (((s2 - s3) * (r1 - r2)) - ((s1 - s2) * (r2 - r3))); var c = t1 - (r1 * a) - (s1 * b); return [ a, b, c ]; }; // Projection // ---------- // An abstract class / interface for projections MM.Projection = function(zoom, transformation) { if (!transformation) { transformation = new MM.Transformation(1, 0, 0, 0, 1, 0); } this.zoom = zoom; this.transformation = transformation; }; MM.Projection.prototype = { zoom: 0, transformation: null, rawProject: function(point) { throw "Abstract method not implemented by subclass."; }, rawUnproject: function(point) { throw "Abstract method not implemented by subclass."; }, project: function(point) { point = this.rawProject(point); if(this.transformation) { point = this.transformation.transform(point); } return point; }, unproject: function(point) { if(this.transformation) { point = this.transformation.untransform(point); } point = this.rawUnproject(point); return point; }, locationCoordinate: function(location) { var point = new MM.Point(Math.PI * location.lon / 180.0, Math.PI * location.lat / 180.0); point = this.project(point); return new MM.Coordinate(point.y, point.x, this.zoom); }, coordinateLocation: function(coordinate) { coordinate = coordinate.zoomTo(this.zoom); var point = new MM.Point(coordinate.column, coordinate.row); point = this.unproject(point); return new MM.Location(180.0 * point.y / Math.PI, 180.0 * point.x / Math.PI); } }; // A projection for equilateral maps, based on longitude and latitude MM.LinearProjection = function(zoom, transformation) { MM.Projection.call(this, zoom, transformation); }; // The Linear projection doesn't reproject points MM.LinearProjection.prototype = { rawProject: function(point) { return new MM.Point(point.x, point.y); }, rawUnproject: function(point) { return new MM.Point(point.x, point.y); } }; MM.extend(MM.LinearProjection, MM.Projection); MM.MercatorProjection = function(zoom, transformation) { // super! MM.Projection.call(this, zoom, transformation); }; // Project lon/lat points into meters required for Mercator MM.MercatorProjection.prototype = { rawProject: function(point) { return new MM.Point(point.x, Math.log(Math.tan(0.25 * Math.PI + 0.5 * point.y))); }, rawUnproject: function(point) { return new MM.Point(point.x, 2 * Math.atan(Math.pow(Math.E, point.y)) - 0.5 * Math.PI); } }; MM.extend(MM.MercatorProjection, MM.Projection); // Providers // --------- // Providers provide tile URLs and possibly elements for layers. MM.MapProvider = function(getTileUrl) { if (getTileUrl) { this.getTileUrl = getTileUrl; } }; MM.MapProvider.prototype = { // defaults to Google-y Mercator style maps projection: new MM.MercatorProjection( 0, MM.deriveTransformation(-Math.PI, Math.PI, 0, 0, Math.PI, Math.PI, 1, 0, -Math.PI, -Math.PI, 0, 1) ), tileWidth: 256, tileHeight: 256, // these are limits for available *tiles* // panning limits will be different (since you can wrap around columns) // but if you put Infinity in here it will screw up sourceCoordinate topLeftOuterLimit: new MM.Coordinate(0,0,0), bottomRightInnerLimit: new MM.Coordinate(1,1,0).zoomTo(18), getTileUrl: function(coordinate) { throw "Abstract method not implemented by subclass."; }, locationCoordinate: function(location) { return this.projection.locationCoordinate(location); }, coordinateLocation: function(coordinate) { return this.projection.coordinateLocation(coordinate); }, outerLimits: function() { return [ this.topLeftOuterLimit.copy(), this.bottomRightInnerLimit.copy() ]; }, // use this to tell MapProvider that tiles only exist between certain zoom levels. // Map will respect thse zoom limits and not allow zooming outside this range setZoomRange: function(minZoom, maxZoom) { this.topLeftOuterLimit = this.topLeftOuterLimit.zoomTo(minZoom); this.bottomRightInnerLimit = this.bottomRightInnerLimit.zoomTo(maxZoom); }, sourceCoordinate: function(coord) { var TL = this.topLeftOuterLimit.zoomTo(coord.zoom); var BR = this.bottomRightInnerLimit.zoomTo(coord.zoom); var vSize = BR.row - TL.row; if (coord.row < 0 | coord.row >= vSize) { // it's too high or too low: return null; } var hSize = BR.column - TL.column; // assume infinite horizontal scrolling var wrappedColumn = coord.column % hSize; while (wrappedColumn < 0) { wrappedColumn += hSize; } return new MM.Coordinate(coord.row, wrappedColumn, coord.zoom); } }; // A simple tileprovider builder that supports `XYZ`-style tiles. MM.TemplatedMapProvider = function(template, subdomains) { MM.MapProvider.call(this, function(coordinate) { coordinate = this.sourceCoordinate(coordinate); if (!coordinate) { return null; } var base = template; if (subdomains && subdomains.length && base.indexOf("{S}") >= 0) { var subdomain = parseInt(coordinate.zoom + coordinate.row + coordinate.column, 10) % subdomains.length; base = base.replace('{S}', subdomains[subdomain]); } return base.replace('{Z}', coordinate.zoom.toFixed(0)) .replace('{X}', coordinate.column.toFixed(0)) .replace('{Y}', coordinate.row.toFixed(0)); }); }; MM.extend(MM.TemplatedMapProvider, MM.MapProvider); // Event Handlers // -------------- // A utility function for finding the offset of the // mouse from the top-left of the page MM.getMousePoint = function(e, map) { // start with just the mouse (x, y) var point = new MM.Point(e.clientX, e.clientY); // correct for scrolled document point.x += document.body.scrollLeft + document.documentElement.scrollLeft; point.y += document.body.scrollTop + document.documentElement.scrollTop; // correct for nested offsets in DOM for (var node = map.parent; node; node = node.offsetParent) { point.x -= node.offsetLeft; point.y -= node.offsetTop; } return point; }; // A handler that allows mouse-wheel zooming - zooming in // when page would scroll up, and out when the page would scroll down. MM.MouseWheelHandler = function(map) { if (map !== undefined) this.init(map); }; MM.MouseWheelHandler.prototype = { init: function(map) { this.map = map; this._mouseWheel = MM.bind(this.mouseWheel, this); MM.addEvent(map.parent, 'mousewheel', this._mouseWheel); }, remove: function() { MM.removeEvent(this.map.parent, 'mousewheel', this._mouseWheel); }, mouseWheel: function(e) { var delta = 0; this.prevTime = this.prevTime || new Date().getTime(); if (e.wheelDelta) { delta = e.wheelDelta; } else if (e.detail) { delta = -e.detail; } // limit mousewheeling to once every 200ms var timeSince = new Date().getTime() - this.prevTime; if (Math.abs(delta) > 0 && (timeSince > 200)) { var point = MM.getMousePoint(e, this.map); this.map.zoomByAbout(delta > 0 ? 1 : -1, point); this.prevTime = new Date().getTime(); } // Cancel the event so that the page doesn't scroll return MM.cancelEvent(e); } }; // Handle double clicks, that zoom the map in one zoom level. MM.DoubleClickHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.DoubleClickHandler.prototype = { init: function(map) { this.map = map; this._doubleClick = MM.bind(this.doubleClick, this); MM.addEvent(map.parent, 'dblclick', this._doubleClick); }, remove: function() { MM.removeEvent(this.map.parent, 'dblclick', this._doubleClick); }, doubleClick: function(e) { // Ensure that this handler is attached once. // Get the point on the map that was double-clicked var point = MM.getMousePoint(e, this.map); // use shift-double-click to zoom out this.map.zoomByAbout(e.shiftKey ? -1 : 1, point); return MM.cancelEvent(e); } }; // Handle the use of mouse dragging to pan the map. MM.DragHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.DragHandler.prototype = { init: function(map) { this.map = map; this._mouseDown = MM.bind(this.mouseDown, this); MM.addEvent(map.parent, 'mousedown', this._mouseDown); }, remove: function() { MM.removeEvent(this.map.parent, 'mousedown', this._mouseDown); }, mouseDown: function(e) { MM.addEvent(document, 'mouseup', this._mouseUp = MM.bind(this.mouseUp, this)); MM.addEvent(document, 'mousemove', this._mouseMove = MM.bind(this.mouseMove, this)); this.prevMouse = new MM.Point(e.clientX, e.clientY); this.map.parent.style.cursor = 'move'; return MM.cancelEvent(e); }, mouseMove: function(e) { if (this.prevMouse) { this.map.panBy( e.clientX - this.prevMouse.x, e.clientY - this.prevMouse.y); this.prevMouse.x = e.clientX; this.prevMouse.y = e.clientY; this.prevMouse.t = +new Date(); } return MM.cancelEvent(e); }, mouseUp: function(e) { MM.removeEvent(document, 'mouseup', this._mouseUp); MM.removeEvent(document, 'mousemove', this._mouseMove); this.prevMouse = null; this.map.parent.style.cursor = ''; return MM.cancelEvent(e); } }; // A shortcut for adding drag, double click, // and mouse wheel events to the map. This is the default // handler attached to a map if the handlers argument isn't given. MM.MouseHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.MouseHandler.prototype = { init: function(map) { this.map = map; this.handlers = [ new MM.DragHandler(map), new MM.DoubleClickHandler(map), new MM.MouseWheelHandler(map) ]; }, remove: function() { for (var i = 0; i < this.handlers.length; i++) { this.handlers[i].remove(); } } }; MM.TouchHandler = function() { }; MM.TouchHandler.prototype = { maxTapTime: 250, maxTapDistance: 30, maxDoubleTapDelay: 350, locations: {}, taps: [], wasPinching: false, lastPinchCenter: null, init: function(map, options) { this.map = map; options = options || {}; this._touchStartMachine = MM.bind(this.touchStartMachine, this); this._touchMoveMachine = MM.bind(this.touchMoveMachine, this); this._touchEndMachine = MM.bind(this.touchEndMachine, this); MM.addEvent(map.parent, 'touchstart', this._touchStartMachine); MM.addEvent(map.parent, 'touchmove', this._touchMoveMachine); MM.addEvent(map.parent, 'touchend', this._touchEndMachine); this.options = {}; this.options.snapToZoom = options.snapToZoom || true; }, remove: function() { MM.removeEvent(this.map.parent, 'touchstart', this._touchStartMachine); MM.removeEvent(this.map.parent, 'touchmove', this._touchMoveMachine); MM.removeEvent(this.map.parent, 'touchend', this._touchEndMachine); }, updateTouches: function(e) { for (var i = 0; i < e.touches.length; i += 1) { var t = e.touches[i]; if (t.identifier in this.locations) { var l = this.locations[t.identifier]; l.x = t.screenX; l.y = t.screenY; l.scale = e.scale; } else { this.locations[t.identifier] = { scale: e.scale, startPos: { x: t.screenX, y: t.screenY }, x: t.screenX, y: t.screenY, time: new Date().getTime() }; } } }, // Test whether touches are from the same source - // whether this is the same touchmove event. sameTouch: function(event, touch) { return (event && event.touch) && (touch.identifier == event.touch.identifier); }, touchStartMachine: function(e) { this.updateTouches(e); return MM.cancelEvent(e); }, touchMoveMachine: function(e) { switch (e.touches.length) { case 1: this.onPanning(e.touches[0]); break; case 2: this.onPinching(e); break; } this.updateTouches(e); return MM.cancelEvent(e); }, touchEndMachine: function(e) { var now = new Date().getTime(); // round zoom if we're done pinching if (e.touches.length === 0 && this.wasPinching) { this.onPinched(this.lastPinchCenter); } // Look at each changed touch in turn. for (var i = 0; i < e.changedTouches.length; i += 1) { var t = e.changedTouches[i], loc = this.locations[t.identifier]; // if we didn't see this one (bug?) // or if it was consumed by pinching already // just skip to the next one if (!loc || loc.wasPinch) { continue; } // we now know we have an event object and a // matching touch that's just ended. Let's see // what kind of event it is based on how long it // lasted and how far it moved. var pos = { x: t.screenX, y: t.screenY }, time = now - loc.time, travel = MM.Point.distance(pos, loc.startPos); if (travel > this.maxTapDistance) { // we will to assume that the drag has been handled separately } else if (time > this.maxTapTime) { // close in space, but not in time: a hold pos.end = now; pos.duration = time; this.onHold(pos); } else { // close in both time and space: a tap pos.time = now; this.onTap(pos); } } // Weird, sometimes an end event doesn't get thrown // for a touch that nevertheless has disappeared. // Still, this will eventually catch those ids: var validTouchIds = {}; for (var j = 0; j < e.touches.length; j++) { validTouchIds[e.touches[j].identifier] = true; } for (var id in this.locations) { if (!(id in validTouchIds)) { delete validTouchIds[id]; } } return MM.cancelEvent(e); }, onHold: function(hold) { // TODO }, // Handle a tap event - mainly watch for a doubleTap onTap: function(tap) { if (this.taps.length && (tap.time - this.taps[0].time) < this.maxDoubleTapDelay) { this.onDoubleTap(tap); this.taps = []; return; } this.taps = [tap]; }, // Handle a double tap by zooming in a single zoom level to a // round zoom. onDoubleTap: function(tap) { var z = this.map.getZoom(), // current zoom tz = Math.round(z) + 1, // target zoom dz = tz - z; // desired delate // zoom in to a round number var p = new MM.Point(tap.x, tap.y); this.map.zoomByAbout(dz, p); }, // Re-transform the actual map parent's CSS transformation onPanning: function(touch) { var pos = { x: touch.screenX, y: touch.screenY }, prev = this.locations[touch.identifier]; this.map.panBy(pos.x - prev.x, pos.y - prev.y); }, onPinching: function(e) { // use the first two touches and their previous positions var t0 = e.touches[0], t1 = e.touches[1], p0 = new MM.Point(t0.screenX, t0.screenY), p1 = new MM.Point(t1.screenX, t1.screenY), l0 = this.locations[t0.identifier], l1 = this.locations[t1.identifier]; // mark these touches so they aren't used as taps/holds l0.wasPinch = true; l1.wasPinch = true; // scale about the center of these touches var center = MM.Point.interpolate(p0, p1, 0.5); this.map.zoomByAbout( Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2, center ); // pan from the previous center of these touches var prevCenter = MM.Point.interpolate(l0, l1, 0.5); this.map.panBy(center.x - prevCenter.x, center.y - prevCenter.y); this.wasPinching = true; this.lastPinchCenter = center; }, // When a pinch event ends, round the zoom of the map. onPinched: function(p) { // TODO: easing if (this.options.snapToZoom) { var z = this.map.getZoom(), // current zoom tz = Math.round(z); // target zoom this.map.zoomByAbout(tz - z, p); } this.wasPinching = false; } }; // CallbackManager // --------------- // A general-purpose event binding manager used by `Map` // and `RequestManager` // Construct a new CallbackManager, with an list of // supported events. MM.CallbackManager = function(owner, events) { this.owner = owner; this.callbacks = {}; for (var i = 0; i < events.length; i++) { this.callbacks[events[i]] = []; } }; // CallbackManager does simple event management for modestmaps MM.CallbackManager.prototype = { // The element on which callbacks will be triggered. owner: null, // An object of callbacks in the form // // { event: function } callbacks: null, // Add a callback to this object - where the `event` is a string of // the event name and `callback` is a function. addCallback: function(event, callback) { if (typeof(callback) == 'function' && this.callbacks[event]) { this.callbacks[event].push(callback); } }, // Remove a callback. The given function needs to be equal (`===`) to // the callback added in `addCallback`, so named functions should be // used as callbacks. removeCallback: function(event, callback) { if (typeof(callback) == 'function' && this.callbacks[event]) { var cbs = this.callbacks[event], len = cbs.length; for (var i = 0; i < len; i++) { if (cbs[i] === callback) { cbs.splice(i,1); break; } } } }, // Trigger a callback, passing it an object or string from the second // argument. dispatchCallback: function(event, message) { if(this.callbacks[event]) { for (var i = 0; i < this.callbacks[event].length; i += 1) { try { this.callbacks[event][i](this.owner, message); } catch(e) { //console.log(e); // meh } } } } }; // RequestManager // -------------- // an image loading queue MM.RequestManager = function() { // The loading bay is a document fragment to optimize appending, since // the elements within are invisible. See // [this blog post](http://ejohn.org/blog/dom-documentfragments/). this.loadingBay = document.createDocumentFragment(); this.requestsById = {}; this.openRequestCount = 0; this.maxOpenRequests = 4; this.requestQueue = []; this.callbackManager = new MM.CallbackManager(this, ['requestcomplete']); }; MM.RequestManager.prototype = { // DOM element, hidden, for making sure images dispatch complete events loadingBay: null, // all known requests, by ID requestsById: null, // current pending requests requestQueue: null, // current open requests (children of loadingBay) openRequestCount: null, // the number of open requests permitted at one time, clamped down // because of domain-connection limits. maxOpenRequests: null, // for dispatching 'requestcomplete' callbackManager: null, addCallback: function(event, callback) { this.callbackManager.addCallback(event,callback); }, removeCallback: function(event, callback) { this.callbackManager.removeCallback(event,callback); }, dispatchCallback: function(event, message) { this.callbackManager.dispatchCallback(event,message); }, // Clear everything in the queue by excluding nothing clear: function() { this.clearExcept({}); }, // Clear everything in the queue except for certain ids, speciied // by an object of the form // // { id: throwawayvalue } clearExcept: function(validIds) { // clear things from the queue first... for (var i = 0; i < this.requestQueue.length; i++) { var request = this.requestQueue[i]; if (request && !(request.id in validIds)) { this.requestQueue[i] = null; } } // then check the loadingBay... var openRequests = this.loadingBay.childNodes; for (var j = openRequests.length-1; j >= 0; j--) { var img = openRequests[j]; if (!(img.id in validIds)) { this.loadingBay.removeChild(img); this.openRequestCount--; /* console.log(this.openRequestCount + " open requests"); */ img.src = img.coord = img.onload = img.onerror = null; } } // hasOwnProperty protects against prototype additions // > "The standard describes an augmentable Object.prototype. // Ignore standards at your own peril." // -- http://www.yuiblog.com/blog/2006/09/26/for-in-intrigue/ for (var id in this.requestsById) { if (this.requestsById.hasOwnProperty(id)) { if (!(id in validIds)) { var requestToRemove = this.requestsById[id]; // whether we've done the request or not... delete this.requestsById[id]; if (requestToRemove !== null) { requestToRemove = requestToRemove.id = requestToRemove.coord = requestToRemove.url = null; } } } } }, // Given a tile id, check whether the RequestManager is currently // requesting it and waiting for the result. hasRequest: function(id) { return (id in this.requestsById); }, // * TODO: remove dependency on coord (it's for sorting, maybe call it data?) // * TODO: rename to requestImage once it's not tile specific requestTile: function(id, coord, url) { if (!(id in this.requestsById)) { var request = { id: id, coord: coord.copy(), url: url }; // if there's no url just make sure we don't request this image again this.requestsById[id] = request; if (url) { this.requestQueue.push(request); /* console.log(this.requestQueue.length + ' pending requests'); */ } } }, getProcessQueue: function() { // let's only create this closure once... if (!this._processQueue) { var theManager = this; this._processQueue = function() { theManager.processQueue(); }; } return this._processQueue; }, // Select images from the `requestQueue` and create image elements for // them, attaching their load events to the function returned by // `this.getLoadComplete()` so that they can be added to the map. processQueue: function(sortFunc) { // When the request queue fills up beyond 8, start sorting the // requests so that spiral-loading or another pattern can be used. if (sortFunc && this.requestQueue.length > 8) { this.requestQueue.sort(sortFunc); } while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) { var request = this.requestQueue.pop(); if (request) { this.openRequestCount++; /* console.log(this.openRequestCount + ' open requests'); */ // JSLitmus benchmark shows createElement is a little faster than // new Image() in Firefox and roughly the same in Safari: // http://tinyurl.com/y9wz2jj http://tinyurl.com/yes6rrt var img = document.createElement('img'); // FIXME: id is technically not unique in document if there // are two Maps but toKey is supposed to be fast so we're trying // to avoid a prefix ... hence we can't use any calls to // `document.getElementById()` to retrieve images img.id = request.id; img.style.position = 'absolute'; // * FIXME: store this elsewhere to avoid scary memory leaks? // * FIXME: call this 'data' not 'coord' so that RequestManager is less Tile-centric? img.coord = request.coord; // add it to the DOM in a hidden layer, this is a bit of a hack, but it's // so that the event we get in image.onload has srcElement assigned in IE6 this.loadingBay.appendChild(img); // set these before img.src to avoid missing an img that's already cached img.onload = img.onerror = this.getLoadComplete(); img.src = request.url; // keep things tidy request = request.id = request.coord = request.url = null; } } }, _loadComplete: null, // Get the singleton `_loadComplete` function that is called on image // load events, either removing them from the queue and dispatching an // event to add them to the map, or deleting them if the image failed // to load. getLoadComplete: function() { // let's only create this closure once... if (!this._loadComplete) { var theManager = this; this._loadComplete = function(e) { // this is needed because we don't use MM.addEvent for images e = e || window.event; // srcElement for IE, target for FF, Safari etc. var img = e.srcElement || e.target; // unset these straight away so we don't call this twice img.onload = img.onerror = null; // pull it back out of the (hidden) DOM // so that draw will add it correctly later theManager.loadingBay.removeChild(img); theManager.openRequestCount--; delete theManager.requestsById[img.id]; /* console.log(theManager.openRequestCount + ' open requests'); */ // NB:- complete is also true onerror if we got a 404 if (e.type === 'load' && (img.complete || (img.readyState && img.readyState == 'complete'))) { theManager.dispatchCallback('requestcomplete', img); } else { // if it didn't finish clear its src to make sure it // really stops loading // FIXME: we'll never retry because this id is still // in requestsById - is that right? img.src = null; } // keep going in the same order // use `setTimeout()` to avoid the IE recursion limit, see // http://cappuccino.org/discuss/2010/03/01/internet-explorer-global-variables-and-stack-overflows/ // and https://github.com/stamen/modestmaps-js/issues/12 setTimeout(theManager.getProcessQueue(), 0); }; } return this._loadComplete; } }; // Map // Instance of a map intended for drawing to a div. // // * `parent` (required DOM element) // Can also be an ID of a DOM element // * `provider` (required MapProvider) // Provides tile URLs and map projections // * `dimensions` (optional Point) // Size of map to create // * `eventHandlers` (optional Array) // If empty or null MouseHandler will be used // Otherwise, each handler will be called with init(map) MM.Map = function(parent, provider, dimensions, eventHandlers) { if (typeof parent == 'string') { parent = document.getElementById(parent); if (!parent) { throw 'The ID provided to modest maps could not be found.'; } } this.parent = parent; // we're no longer adding width and height to parent.style but we still // need to enforce padding, overflow and position otherwise everything screws up // TODO: maybe console.warn if the current values are bad? this.parent.style.padding = '0'; this.parent.style.overflow = 'hidden'; var position = MM.getStyle(this.parent, 'position'); if (position != 'relative' && position != 'absolute') { this.parent.style.position = 'relative'; } // if you don't specify dimensions we assume you want to fill the parent // unless the parent has no w/h, in which case we'll still use a default if (!dimensions) { dimensions = new MM.Point( this.parent.offsetWidth, this.parent.offsetHeight); this.autoSize = true; // FIXME: listeners like this will stop the map being removed cleanly? // when does removeEvent get called? var theMap = this; MM.addEvent(window, 'resize', this.windowResize()); } else { this.autoSize = false; this.parent.style.width = Math.round(dimensions.x) + 'px'; this.parent.style.height = Math.round(dimensions.y) + 'px'; } this.dimensions = dimensions; this.requestManager = new MM.RequestManager(this.parent); this.requestManager.addCallback('requestcomplete', this.getTileComplete()); this.layers = {}; this.layerParent = document.createElement('div'); this.layerParent.id = this.parent.id + '-layers'; // this text is also used in createOrGetLayer this.layerParent.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0'; this.parent.appendChild(this.layerParent); this.coordinate = new MM.Coordinate(0.5, 0.5, 0); this.setProvider(provider); this.enablePyramidLoading = false; this.callbackManager = new MM.CallbackManager(this, [ 'zoomed', 'panned', 'centered', 'extentset', 'resized', 'drawn' ]); // set up handlers last so that all required attributes/functions are in place if needed if (eventHandlers === undefined) { this.eventHandlers = []; this.eventHandlers.push(new MM.MouseHandler(this)); } else { this.eventHandlers = eventHandlers; if (eventHandlers instanceof Array) { for (var i = 0; i < eventHandlers.length; i++) { eventHandlers[i].init(this); } } } }; MM.Map.prototype = { parent: null, provider: null, dimensions: null, coordinate: null, tiles: null, layers: null, layerParent: null, requestManager: null, tileCacheSize: null, maxTileCacheSize: null, recentTiles: null, recentTilesById: null, recentTileSize: null, callbackManager: null, eventHandlers: null, autoSize: null, toString: function() { return 'Map(#' + this.parent.id + ')'; }, // callbacks... addCallback: function(event, callback) { this.callbackManager.addCallback(event, callback); return this; }, removeCallback: function(event, callback) { this.callbackManager.removeCallback(event, callback); return this; }, dispatchCallback: function(event, message) { this.callbackManager.dispatchCallback(event, message); return this; }, windowResize: function() { if (!this._windowResize) { var theMap = this; this._windowResize = function(event) { // don't call setSize here because it sets parent.style.width/height // and setting the height breaks percentages and default styles theMap.dimensions = new MM.Point(theMap.parent.offsetWidth, theMap.parent.offsetHeight); theMap.draw(); theMap.dispatchCallback('resized', [theMap.dimensions]); }; } return this._windowResize; }, // zooming zoomBy: function(zoomOffset) { this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); MM.getFrame(this.getRedraw()); this.dispatchCallback('zoomed', zoomOffset); return this; }, zoomIn: function() { return this.zoomBy(1); }, zoomOut: function() { return this.zoomBy(-1); }, setZoom: function(z) { return this.zoomBy(z - this.coordinate.zoom); }, zoomByAbout: function(zoomOffset, point) { var location = this.pointLocation(point); this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); var newPoint = this.locationPoint(location); this.dispatchCallback('zoomed', zoomOffset); return this.panBy(point.x - newPoint.x, point.y - newPoint.y); }, // panning panBy: function(dx, dy) { this.coordinate.column -= dx / this.provider.tileWidth; this.coordinate.row -= dy / this.provider.tileHeight; this.coordinate = this.enforceLimits(this.coordinate); // Defer until the browser is ready to draw. MM.getFrame(this.getRedraw()); this.dispatchCallback('panned', [dx, dy]); return this; }, /* panZoom: function(dx, dy, zoom) { this.coordinate.column -= dx / this.provider.tileWidth; this.coordinate.row -= dy / this.provider.tileHeight; this.coordinate = this.coordinate.zoomTo(zoom); // Defer until the browser is ready to draw. MM.getFrame(this.getRedraw()); this.dispatchCallback('panned', [dx, dy]); return this; }, */ panLeft: function() { return this.panBy(100, 0); }, panRight: function() { return this.panBy(-100, 0); }, panDown: function() { return this.panBy(0, -100); }, panUp: function() { return this.panBy(0, 100); }, // positioning setCenter: function(location) { return this.setCenterZoom(location, this.coordinate.zoom); }, setCenterZoom: function(location, zoom) { this.coordinate = this.provider.locationCoordinate(location).zoomTo(parseFloat(zoom) || 0); this.draw(); this.dispatchCallback('centered', [location, zoom]); return this; }, setExtent: function(locations, any) { var TL, BR; for (var i = 0; i < locations.length; i++) { var coordinate = this.provider.locationCoordinate(locations[i]); if (TL) { TL.row = Math.min(TL.row, coordinate.row); TL.column = Math.min(TL.column, coordinate.column); TL.zoom = Math.min(TL.zoom, coordinate.zoom); BR.row = Math.max(BR.row, coordinate.row); BR.column = Math.max(BR.column, coordinate.column); BR.zoom = Math.max(BR.zoom, coordinate.zoom); } else { TL = coordinate.copy(); BR = coordinate.copy(); } } var width = this.dimensions.x + 1; var height = this.dimensions.y + 1; // multiplication factor between horizontal span and map width var hFactor = (BR.column - TL.column) / (width / this.provider.tileWidth); // multiplication factor expressed as base-2 logarithm, for zoom difference var hZoomDiff = Math.log(hFactor) / Math.log(2); // possible horizontal zoom to fit geographical extent in map width var hPossibleZoom = TL.zoom - (any ? hZoomDiff : Math.ceil(hZoomDiff)); // multiplication factor between vertical span and map height var vFactor = (BR.row - TL.row) / (height / this.provider.tileHeight); // multiplication factor expressed as base-2 logarithm, for zoom difference var vZoomDiff = Math.log(vFactor) / Math.log(2); // possible vertical zoom to fit geographical extent in map height var vPossibleZoom = TL.zoom - (any ? vZoomDiff : Math.ceil(vZoomDiff)); // initial zoom to fit extent vertically and horizontally var initZoom = Math.min(hPossibleZoom, vPossibleZoom); // additionally, make sure it's not outside the boundaries set by provider limits // this also catches Infinity stuff initZoom = Math.min(initZoom, this.provider.outerLimits()[1].zoom); initZoom = Math.max(initZoom, this.provider.outerLimits()[0].zoom); // coordinate of extent center var centerRow = (TL.row + BR.row) / 2; var centerColumn = (TL.column + BR.column) / 2; var centerZoom = TL.zoom; this.coordinate = new MM.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom); this.draw(); // draw calls enforceLimits // (if you switch to getFrame, call enforceLimits first) this.dispatchCallback('extentset', locations); return this; }, // Resize the map's container `
`, redrawing the map and triggering // `resized` to make sure that the map's presentation is still correct. setSize: function(dimensionsOrX, orY) { if (dimensionsOrX.hasOwnProperty('x') && dimensionsOrX.hasOwnProperty('y')) { this.dimensions = dimensionsOrX; } else if (orY !== undefined && !isNaN(orY)) { this.dimensions = new MM.Point(dimensionsOrX, orY); } this.parent.style.width = Math.round(this.dimensions.x) + 'px'; this.parent.style.height = Math.round(this.dimensions.y) + 'px'; this.draw(); this.dispatchCallback('resized', [this.dimensions]); return this; }, // projecting points on and off screen coordinatePoint: function(coord) { // Return an x, y point on the map image for a given coordinate. if (coord.zoom != this.coordinate.zoom) { coord = coord.zoomTo(this.coordinate.zoom); } // distance from the center of the map var point = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2); point.x += this.provider.tileWidth * (coord.column - this.coordinate.column); point.y += this.provider.tileHeight * (coord.row - this.coordinate.row); return point; }, // Get a `MM.Coordinate` from an `MM.Point` - returns a new tile-like object // from a screen point. pointCoordinate: function(point) { // new point coordinate reflecting distance from map center, in tile widths var coord = this.coordinate.copy(); coord.column += (point.x - this.dimensions.x / 2) / this.provider.tileWidth; coord.row += (point.y - this.dimensions.y / 2) / this.provider.tileHeight; return coord; }, // Return an x, y point on the map image for a given geographical location. locationPoint: function(location) { return this.coordinatePoint(this.provider.locationCoordinate(location)); }, // Return a geographical location on the map image for a given x, y point. pointLocation: function(point) { return this.provider.coordinateLocation(this.pointCoordinate(point)); }, // inspecting getExtent: function() { var extent = []; extent.push(this.pointLocation(new MM.Point(0, 0))); extent.push(this.pointLocation(this.dimensions)); return extent; }, // Get the current centerpoint of the map, returning a `Location` getCenter: function() { return this.provider.coordinateLocation(this.coordinate); }, // Get the current zoom level of the map, returning a number getZoom: function() { return this.coordinate.zoom; }, // Replace the existing provider or set a provider on the map, clearing // out existing tiles and requests. setProvider: function(newProvider) { var firstProvider = false; if (this.provider === null) { firstProvider = true; } // if we already have a provider the we'll need to // clear the DOM, cancel requests and redraw if (!firstProvider) { this.requestManager.clear(); for (var name in this.layers) { if (this.layers.hasOwnProperty(name)) { var layer = this.layers[name]; while (layer.firstChild) { layer.removeChild(layer.firstChild); } } } } // first provider or not we'll init/reset some values... this.tiles = {}; this.tileCacheSize = 0; this.maxTileCacheSize = 64; this.recentTiles = []; this.recentTilesById = {}; // for later: check geometry of old provider and set a new coordinate center // if needed (now? or when?) this.provider = newProvider; if (!firstProvider) { this.draw(); } return this; }, // stats /* getStats: function() { return { 'Request Queue Length': this.requestManager.requestQueue.length, 'Open Request Count': this.requestManager.requestCount, 'Tile Cache Size': this.tileCacheSize, 'Tiles On Screen': this.parent.getElementsByTagName('img').length }; },*/ // Prevent the user from navigating the map outside the `outerLimits` // of the map's provider. enforceLimits: function(coord) { coord = coord.copy(); var limits = this.provider.outerLimits(); if (limits) { var minZoom = limits[0].zoom; var maxZoom = limits[1].zoom; if (coord.zoom < minZoom) { coord = coord.zoomTo(minZoom); } else if (coord.zoom > maxZoom) { coord = coord.zoomTo(maxZoom); } } return coord; }, // Redraw the tiles on the map, reusing existing tiles. draw: function() { // make sure we're not too far in or out: this.coordinate = this.enforceLimits(this.coordinate); // if we're in between zoom levels, we need to choose the nearest: var baseZoom = Math.round(this.coordinate.zoom); // if we don't have dimensions, check the parent size if (this.dimensions.x <= 0 || this.dimensions.y <= 0) { if (this.autoSize) { // maybe the parent size has changed? var w = this.parent.offsetWidth, h = this.parent.offsetHeight this.dimensions = new MM.Point(w,h); if (w <= 0 || h <= 0) { return; } } else { // the issue can only be corrected with setSize return; } } // these are the top left and bottom right tile coordinates // we'll be loading everything in between: var startCoord = this.pointCoordinate(new MM.Point(0, 0)).zoomTo(baseZoom).container(); var endCoord = this.pointCoordinate(this.dimensions).zoomTo(baseZoom).container().right().down(); var tilePadding = 0; if (tilePadding) { startCoord = startCoord.left(tilePadding).up(tilePadding); endCoord = endCoord.right(tilePadding).down(tilePadding); } // tiles with invalid keys will be removed from visible layers // requests for tiles with invalid keys will be canceled // (this object maps from a tile key to a boolean) var validTileKeys = { }; // make sure we have a container for tiles in the current layer var thisLayer = this.createOrGetLayer(startCoord.zoom); // use this coordinate for generating keys, parents and children: var tileCoord = startCoord.copy(); for (tileCoord.column = startCoord.column; tileCoord.column <= endCoord.column; tileCoord.column += 1) { for (tileCoord.row = startCoord.row; tileCoord.row <= endCoord.row; tileCoord.row += 1) { var tileKey = tileCoord.toKey(); validTileKeys[tileKey] = true; if (tileKey in this.tiles) { var tile = this.tiles[tileKey]; // ensure it's in the DOM: if (tile.parentNode != thisLayer) { thisLayer.appendChild(tile); } } else { if (!this.requestManager.hasRequest(tileKey)) { var tileURL = this.provider.getTileUrl(tileCoord); this.requestManager.requestTile(tileKey, tileCoord, tileURL); } // look for a parent tile in our image cache var tileCovered = false; var maxStepsOut = tileCoord.zoom; for (var pz = 1; pz <= maxStepsOut; pz++) { var parentCoord = tileCoord.zoomBy(-pz).container(); var parentKey = parentCoord.toKey(); if (this.enablePyramidLoading) { // mark all parent tiles valid validTileKeys[parentKey] = true; var parentLayer = this.createOrGetLayer(parentCoord.zoom); /* parentLayer.coordinate = parentCoord.copy(); */ if (parentKey in this.tiles) { var parentTile = this.tiles[parentKey]; if (parentTile.parentNode != parentLayer) { parentLayer.appendChild(parentTile); } } else if (!this.requestManager.hasRequest(parentKey)) { // force load of parent tiles we don't already have this.requestManager.requestTile(parentKey, parentCoord, this.provider.getTileUrl(parentCoord)); } } else { // only mark it valid if we have it already if (parentKey in this.tiles) { validTileKeys[parentKey] = true; tileCovered = true; break; } } } // if we didn't find a parent, look at the children: if (!tileCovered && !this.enablePyramidLoading) { var childCoord = tileCoord.zoomBy(1); // mark everything valid whether or not we have it: validTileKeys[childCoord.toKey()] = true; childCoord.column += 1; validTileKeys[childCoord.toKey()] = true; childCoord.row += 1; validTileKeys[childCoord.toKey()] = true; childCoord.column -= 1; validTileKeys[childCoord.toKey()] = true; } } } } // i from i to zoom-5 are layers that would be scaled too big, // i from zoom+2 to layers.length are layers that would be // scaled too small (and tiles would be too numerous) for (var name in this.layers) { if (this.layers.hasOwnProperty(name)) { var zoom = parseInt(name, 10); if (zoom >= startCoord.zoom - 5 && zoom < startCoord.zoom + 2) { continue; } var layer = this.layers[name]; layer.style.display = 'none'; var visibleTiles = layer.getElementsByTagName('img'); for (var j = visibleTiles.length - 1; j >= 0; j--) { layer.removeChild(visibleTiles[j]); } } } // for tracking time of tile usage: var now = new Date().getTime(); // layers we want to see, if they have tiles in validTileKeys var minLayer = startCoord.zoom - 5; var maxLayer = startCoord.zoom + 2; for (var i = minLayer; i < maxLayer; i++) { var layer = this.layers[i]; if (!layer) { // no tiles for this layer yet continue; } // getElementsByTagName is x10 faster than childNodes, and // let's reuse the access. var scale = 1, theCoord = this.coordinate.copy(), visibleTiles = layer.getElementsByTagName('img'); if (visibleTiles.length > 0) { layer.style.display = 'block'; scale = Math.pow(2, this.coordinate.zoom - i); theCoord = theCoord.zoomTo(i); } else { layer.style.display = 'none'; } var tileWidth = this.provider.tileWidth * scale, tileHeight = this.provider.tileHeight * scale, center = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2); for (var j = visibleTiles.length - 1; j >= 0; j--) { var tile = visibleTiles[j]; if (!validTileKeys[tile.id]) { layer.removeChild(tile); } else { // position tiles MM.moveElement(tile, { x: Math.round(center.x + (tile.coord.column - theCoord.column) * tileWidth), y: Math.round(center.y + (tile.coord.row - theCoord.row) * tileHeight), scale: scale, width: this.provider.tileWidth, height: this.provider.tileHeight }); // log last-touched-time of currently cached tiles this.recentTilesById[tile.id].lastTouchedTime = now; } } } // cancel requests that aren't visible: this.requestManager.clearExcept(validTileKeys); // get newly requested tiles, sort according to current view: this.requestManager.processQueue(this.getCenterDistanceCompare()); // make sure we don't have too much stuff: this.checkCache(); this.dispatchCallback('drawn'); }, _tileComplete: null, getTileComplete: function() { if (!this._tileComplete) { var theMap = this; this._tileComplete = function(manager, tile) { // cache the tile itself: theMap.tiles[tile.id] = tile; theMap.tileCacheSize++; // also keep a record of when we last touched this tile: var record = { id: tile.id, lastTouchedTime: new Date().getTime() }; theMap.recentTilesById[tile.id] = record; theMap.recentTiles.push(record); var theCoord = theMap.coordinate.zoomTo(tile.coord.zoom); var scale = Math.pow(2, theMap.coordinate.zoom - tile.coord.zoom); var tx = ((theMap.dimensions.x / 2) + (tile.coord.column - theCoord.column) * theMap.provider.tileWidth * scale); var ty = ((theMap.dimensions.y / 2) + (tile.coord.row - theCoord.row) * theMap.provider.tileHeight * scale); MM.moveElement(tile, { x: Math.round(tx), y: Math.round(ty), scale: scale, // TODO: pass only scale or only w/h width: theMap.provider.tileWidth, height: theMap.provider.tileHeight }); // Support style transition if available. // add tile to its layer var theLayer = theMap.layers[tile.coord.zoom]; theLayer.appendChild(tile); tile.className = 'map-tile-loaded'; // ensure the layer is visible if it's still the current layer if (Math.round(theMap.coordinate.zoom) === tile.coord.zoom) { theLayer.style.display = 'block'; } // request a lazy redraw of all layers // this will remove tiles that were only visible // to cover this tile while it loaded: theMap.requestRedraw(); }; } return this._tileComplete; }, _redrawTimer: undefined, requestRedraw: function() { // we'll always draw within 1 second of this request, // sometimes faster if there's already a pending redraw // this is used when a new tile arrives so that we clear // any parent/child tiles that were only being displayed // until the tile loads at the right zoom level if (!this._redrawTimer) { this._redrawTimer = setTimeout(this.getRedraw(), 1000); } }, _redraw: null, getRedraw: function() { // let's only create this closure once... if (!this._redraw) { var theMap = this; this._redraw = function() { theMap.draw(); theMap._redrawTimer = 0; }; } return this._redraw; }, createOrGetLayer: function(zoom) { if (zoom in this.layers) { return this.layers[zoom]; } //console.log('creating layer ' + zoom); var layer = document.createElement('div'); layer.id = this.parent.id + '-zoom-' + zoom; layer.style.cssText = this.layerParent.style.cssText; layer.style.zIndex = zoom; this.layerParent.appendChild(layer); this.layers[zoom] = layer; return layer; }, // keeps cache below max size // (called every time we receive a new tile and add it to the cache) checkCache: function() { var numTilesOnScreen = this.parent.getElementsByTagName('img').length; var maxTiles = Math.max(numTilesOnScreen, this.maxTileCacheSize); if (this.tileCacheSize > maxTiles) { // sort from newest (highest) to oldest (lowest) this.recentTiles.sort(function(t1, t2) { return t2.lastTouchedTime < t1.lastTouchedTime ? -1 : t2.lastTouchedTime > t1.lastTouchedTime ? 1 : 0; }); } while (this.tileCacheSize > maxTiles) { // delete the oldest record var tileRecord = this.recentTiles.pop(); var now = new Date().getTime(); delete this.recentTilesById[tileRecord.id]; /*window.console.log('removing ' + tileRecord.id + ' last seen ' + (now-tileRecord.lastTouchedTime) + 'ms ago'); */ // now actually remove it from the cache... var tile = this.tiles[tileRecord.id]; if (tile.parentNode) { // I'm leaving this uncommented for now but you should never see it: alert("Gah: trying to removing cached tile even though it's still in the DOM"); } else { delete this.tiles[tileRecord.id]; this.tileCacheSize--; } } }, // Compares manhattan distance from center of // requested tiles to current map center // NB:- requested tiles are *popped* from queue, so we do a descending sort getCenterDistanceCompare: function() { var theCoord = this.coordinate.zoomTo(Math.round(this.coordinate.zoom)); return function(r1, r2) { if (r1 && r2) { var c1 = r1.coord; var c2 = r2.coord; if (c1.zoom == c2.zoom) { var ds1 = Math.abs(theCoord.row - c1.row - 0.5) + Math.abs(theCoord.column - c1.column - 0.5); var ds2 = Math.abs(theCoord.row - c2.row - 0.5) + Math.abs(theCoord.column - c2.column - 0.5); return ds1 < ds2 ? 1 : ds1 > ds2 ? -1 : 0; } else { return c1.zoom < c2.zoom ? 1 : c1.zoom > c2.zoom ? -1 : 0; } } return r1 ? 1 : r2 ? -1 : 0; }; }, // Attempts to destroy all attachment a map has to a page // and clear its memory usage. destroy: function() { this.requestManager.clear(); for (var i = 0; i < this.eventHandlers.length; i++) { this.eventHandlers[i].remove(); } this.parent.removeChild(this.layerParent); MM.removeEvent(window, 'resize', this.windowResize()); return this; } }; if (typeof module !== 'undefined' && module.exports) { module.exports = { Point: MM.Point, Projection: MM.Projection, MercatorProjection: MM.MercatorProjection, LinearProjection: MM.LinearProjection, Transformation: MM.Transformation, Location: MM.Location, MapProvider: MM.MapProvider, TemplatedMapProvider: MM.TemplatedMapProvider, Coordinate: MM.Coordinate }; } })(com.modestmaps); stamen-modestmaps-js-52d7710/Makefile0000664000175000017500000000125611653546654016465 0ustar daviddavidJS_FILES = \ src/start.js \ src/utils.js \ src/point.js \ src/coordinate.js \ src/location.js \ src/transformation.js \ src/projection.js \ src/provider.js \ src/mouse.js \ src/touch.js \ src/callbacks.js \ src/requests.js \ src/map.js \ src/end.js modestmaps.min.js: modestmaps.js rm -f modestmaps.min.js java -jar tools/yuicompressor-2.4.2.jar modestmaps.js > modestmaps.min.js chmod a-w modestmaps.min.js modestmaps.js: $(JS_FILES) Makefile rm -f modestmaps.js cat $(JS_FILES) >> modestmaps.js chmod a-w modestmaps.js clean: rm modestmaps.js rm modestmaps.min.js doc: ./node_modules/.bin/docco src/*.js tests: ./node_modules/.bin/expresso test/*.test.js stamen-modestmaps-js-52d7710/CHANGELOG0000664000175000017500000001560711653546654016244 0ustar daviddavidModest Maps JS Changelog. Following the semantic versioning recommendation best we can: "Consider a version format of X.Y.Z (Major.Minor.Patch). Bug fixes not affecting the API increment the patch version, backwards compatible API additions/changes increment the minor version, and backwards incompatible API changes increment the major version." -- http://semver.org/ v0.21.0 - Returns `this` from `map.addCallback()`, `map.removeCallback()`, and `map.dispatchCallback()` v0.20.0 - Adds `map.destroy()` method, and in that process, allows all handlers to be removable with a `.remove()` function. v0.19.1 - Removes unused 'parent' argument to `com.modestmaps.RequestManager` v0.19.0 - Adds a second parameter to `map.setExtent()` which, if true, allows the extent set to place the map at a non-integer zoom level. By default, `setEvent` will behave as before. v0.18.5 - Address issue where display:none on parent affects size detection (thanks @tmcw) - Remove arbitary default map size - parent size is used always unless dimensions are specified. Use setSize to override at any time. v0.18.4 - Fix Location.interpolate between the same location - Fix DragHandler - remove inertia code v0.18.3 - enforce limits when zooming and panning so that map functions are correct when returning and before the deferred draw call using getFrame v0.18.2 - revert to @tmcw's original getFrame (simpler version broke in Chrome) v0.18.1 - revert Coordinate.toKey to string joining - fix up utils.js functions so that tests run again - modify double-tap thresholds and adjust onPinched behavior - a few more cleanups in TouchHandler (mainly to use MM.Point functions) v0.18.0 - many tiny formatting/syntax fixes; thx JSHint! - MM.moveElement is used for tile positioning - MM.moveElement detects and uses CSS transforms wherever possible - MM.bind is used for function binding instead of awkward closures - MM.getFrame is used to request redraws - MM.getFrame uses provisional requestAnimationFrame implementations if possible - TouchHandler is in the default build of modestmaps.js - Coordinate.toKey() is clever again ;) Thanks to @tmcw for the majority of these contributions. They were contributed on a branch, hence only one minor version number bump. As far as we know this should all be backwards compatible. If we're mistaken, please file an issue at https://github.com/stamen/modestmaps-js/issues Note also that we've now got fairly decent node.js support for the core MM.js classes, and there's an example of how to use Modest Maps with node-canvas to render static map images from a tiled data source. "npm install modestmaps" for node.js fans :) v0.17.0 - Mouse handlers refactor: while MouseHandler is still available for adding all handlers to the map, double-click, zoomwheel, and drag handlers are available individually. - Broken images that result from image loading errors are no longer added to the map. v0.16.1 - Unimplemented abstract methods throw exceptions instead of calling `alert()` v0.16.0 - added MapProvider.setZoomRange for @straup v0.15.2 - misc syntax fixes and improve parseInt/radix correctness, from @tmcw v0.15.1 - switched to document fragment for loadingBay, from @tmcw v0.15.0 - added method chaining from @tmcw v0.14.3 - improve redraw behavior by ensuring layer is visible in getTileComplete - use a closure in v0.14.2's setTimeout to ensure proper 'this' v0.14.2 - add setTimeout to processQueue to avoid stack overflow/recursion bug in IE 7/8 (https://github.com/stamen/modestmaps-js/issues/12) thanks @yhahn! v0.14.1 - reinstated display of children for missing tiles, except when if map.enablePyramidLoading is set to true v0.14.0 - added map.enablePyramidLoading flag for pyramid loading (off by default) - fixed Coordinate.prototype.toKey (back to string join to avoid collisions) v0.13.5 - changed the order of initialization so that event handlers go last (this is so that attributes like layerParent are in place for complex event handlers like anyzoom.js... handlers can also use callbacks now) 2011-01-03 note: - broke modestmap.js file into src folder, now building with Makefile - no functionality change to modestmaps.js (just whitespace changes) v0.13.4 - changed to img.style.width instead of img.width, for ipads (see examples/touch/test.html) v0.13.3 - stubbed out tilePadding into Map.draw v0.13.2 - removing magic numbers from Mercator projection for parity with Python version v0.13.1 - rejiggered the Map's draw function to be a bit clearer - removed superfluous layer.coordinate (internal only) v0.13.0 - factored image loading out into a separate RequestManager - cleaned up RequestManager to be less tile-centric, more img-centric v0.12.0 - made callback handling more modular v0.11.2 - re-instated zoom level check for setExtent (fixes bug where locations are all the same) v0.11.1 - moved to cssText for long CSS inits v0.11.0 - added 'drawn' callback - added removeCallback - correctly following semver.org now, incrementing minor version! v0.10.4 - modified queue sorting to support pyramid loading - stubbed out pyramid loading - tidied up draw code to be clearer (wantedTiles --> validTileKeys) v0.10.3 - added Point.distance and Point.interpolate - added Location.distance and Location.interpolate v0.10.2 - tweak to sorting function that appears to fix an issue in IE8 v0.10.1 - fixed tile sorting for in-between zoom levels - fixed tile position in onload handler v0.10.0 - tidied up initial coord/position maths, now supports arbitrary zoom levels (with seams) v0.9.5 v0.9.4 - changes to MapProvider.sourceCoordinate to support non-Mercator bounds v0.9.3 - added inner and outer limits to providers and enforcing min/max zoom in Map.draw() - changed zoomByAbout to use zoomBy (and draw/enforceLimits) before applying panBy v0.9.2 - fixed bug that could break zooming if setCenterZoom was called with a string v0.9.1 - removed assumption that layers go from 0 to 20 v0.9.0 - added rational version numbering May 2010, pre-semver + added setSize and setProvider methods (fixed the latter so that all loads are canceled) + optional interaction (factored out mouse handling... enables touch or keyboard later) + made a touch handler for iphads + made a TemplatedMapProvider + made a demo that accepts a URL template for a map provider + started to work towards jslint conformance + started to move to 80 character line length where practical + made a demo with two maps, synchronized + jslint fixes, 80 char lines, better sorting, removed createOverlay + templated providers wrap around in longitude by default + added a demo keyboard handler + positioning single tiles in onload instead of redrawing everything + added a zoom box demo + make sure the cache gets cleared after a while (BIG FEATURE) + don't load above/below north/south poles (for default mercator maps) + added a random subdomain helper to templated providers stamen-modestmaps-js-52d7710/.gitignore0000664000175000017500000000001511653546654017005 0ustar daviddavidnode_modules stamen-modestmaps-js-52d7710/src/0000775000175000017500000000000011653546654015610 5ustar daviddavidstamen-modestmaps-js-52d7710/src/provider.js0000664000175000017500000000676711653546654020020 0ustar daviddavid // Providers // --------- // Providers provide tile URLs and possibly elements for layers. MM.MapProvider = function(getTileUrl) { if (getTileUrl) { this.getTileUrl = getTileUrl; } }; MM.MapProvider.prototype = { // defaults to Google-y Mercator style maps projection: new MM.MercatorProjection( 0, MM.deriveTransformation(-Math.PI, Math.PI, 0, 0, Math.PI, Math.PI, 1, 0, -Math.PI, -Math.PI, 0, 1) ), tileWidth: 256, tileHeight: 256, // these are limits for available *tiles* // panning limits will be different (since you can wrap around columns) // but if you put Infinity in here it will screw up sourceCoordinate topLeftOuterLimit: new MM.Coordinate(0,0,0), bottomRightInnerLimit: new MM.Coordinate(1,1,0).zoomTo(18), getTileUrl: function(coordinate) { throw "Abstract method not implemented by subclass."; }, locationCoordinate: function(location) { return this.projection.locationCoordinate(location); }, coordinateLocation: function(coordinate) { return this.projection.coordinateLocation(coordinate); }, outerLimits: function() { return [ this.topLeftOuterLimit.copy(), this.bottomRightInnerLimit.copy() ]; }, // use this to tell MapProvider that tiles only exist between certain zoom levels. // Map will respect thse zoom limits and not allow zooming outside this range setZoomRange: function(minZoom, maxZoom) { this.topLeftOuterLimit = this.topLeftOuterLimit.zoomTo(minZoom); this.bottomRightInnerLimit = this.bottomRightInnerLimit.zoomTo(maxZoom); }, sourceCoordinate: function(coord) { var TL = this.topLeftOuterLimit.zoomTo(coord.zoom); var BR = this.bottomRightInnerLimit.zoomTo(coord.zoom); var vSize = BR.row - TL.row; if (coord.row < 0 | coord.row >= vSize) { // it's too high or too low: return null; } var hSize = BR.column - TL.column; // assume infinite horizontal scrolling var wrappedColumn = coord.column % hSize; while (wrappedColumn < 0) { wrappedColumn += hSize; } return new MM.Coordinate(coord.row, wrappedColumn, coord.zoom); } }; // A simple tileprovider builder that supports `XYZ`-style tiles. MM.TemplatedMapProvider = function(template, subdomains) { MM.MapProvider.call(this, function(coordinate) { coordinate = this.sourceCoordinate(coordinate); if (!coordinate) { return null; } var base = template; if (subdomains && subdomains.length && base.indexOf("{S}") >= 0) { var subdomain = parseInt(coordinate.zoom + coordinate.row + coordinate.column, 10) % subdomains.length; base = base.replace('{S}', subdomains[subdomain]); } return base.replace('{Z}', coordinate.zoom.toFixed(0)) .replace('{X}', coordinate.column.toFixed(0)) .replace('{Y}', coordinate.row.toFixed(0)); }); }; MM.extend(MM.TemplatedMapProvider, MM.MapProvider); stamen-modestmaps-js-52d7710/src/touch.js0000664000175000017500000001715211653546654017276 0ustar daviddavid MM.TouchHandler = function() { }; MM.TouchHandler.prototype = { maxTapTime: 250, maxTapDistance: 30, maxDoubleTapDelay: 350, locations: {}, taps: [], wasPinching: false, lastPinchCenter: null, init: function(map, options) { this.map = map; options = options || {}; this._touchStartMachine = MM.bind(this.touchStartMachine, this); this._touchMoveMachine = MM.bind(this.touchMoveMachine, this); this._touchEndMachine = MM.bind(this.touchEndMachine, this); MM.addEvent(map.parent, 'touchstart', this._touchStartMachine); MM.addEvent(map.parent, 'touchmove', this._touchMoveMachine); MM.addEvent(map.parent, 'touchend', this._touchEndMachine); this.options = {}; this.options.snapToZoom = options.snapToZoom || true; }, remove: function() { MM.removeEvent(this.map.parent, 'touchstart', this._touchStartMachine); MM.removeEvent(this.map.parent, 'touchmove', this._touchMoveMachine); MM.removeEvent(this.map.parent, 'touchend', this._touchEndMachine); }, updateTouches: function(e) { for (var i = 0; i < e.touches.length; i += 1) { var t = e.touches[i]; if (t.identifier in this.locations) { var l = this.locations[t.identifier]; l.x = t.screenX; l.y = t.screenY; l.scale = e.scale; } else { this.locations[t.identifier] = { scale: e.scale, startPos: { x: t.screenX, y: t.screenY }, x: t.screenX, y: t.screenY, time: new Date().getTime() }; } } }, // Test whether touches are from the same source - // whether this is the same touchmove event. sameTouch: function(event, touch) { return (event && event.touch) && (touch.identifier == event.touch.identifier); }, touchStartMachine: function(e) { this.updateTouches(e); return MM.cancelEvent(e); }, touchMoveMachine: function(e) { switch (e.touches.length) { case 1: this.onPanning(e.touches[0]); break; case 2: this.onPinching(e); break; } this.updateTouches(e); return MM.cancelEvent(e); }, touchEndMachine: function(e) { var now = new Date().getTime(); // round zoom if we're done pinching if (e.touches.length === 0 && this.wasPinching) { this.onPinched(this.lastPinchCenter); } // Look at each changed touch in turn. for (var i = 0; i < e.changedTouches.length; i += 1) { var t = e.changedTouches[i], loc = this.locations[t.identifier]; // if we didn't see this one (bug?) // or if it was consumed by pinching already // just skip to the next one if (!loc || loc.wasPinch) { continue; } // we now know we have an event object and a // matching touch that's just ended. Let's see // what kind of event it is based on how long it // lasted and how far it moved. var pos = { x: t.screenX, y: t.screenY }, time = now - loc.time, travel = MM.Point.distance(pos, loc.startPos); if (travel > this.maxTapDistance) { // we will to assume that the drag has been handled separately } else if (time > this.maxTapTime) { // close in space, but not in time: a hold pos.end = now; pos.duration = time; this.onHold(pos); } else { // close in both time and space: a tap pos.time = now; this.onTap(pos); } } // Weird, sometimes an end event doesn't get thrown // for a touch that nevertheless has disappeared. // Still, this will eventually catch those ids: var validTouchIds = {}; for (var j = 0; j < e.touches.length; j++) { validTouchIds[e.touches[j].identifier] = true; } for (var id in this.locations) { if (!(id in validTouchIds)) { delete validTouchIds[id]; } } return MM.cancelEvent(e); }, onHold: function(hold) { // TODO }, // Handle a tap event - mainly watch for a doubleTap onTap: function(tap) { if (this.taps.length && (tap.time - this.taps[0].time) < this.maxDoubleTapDelay) { this.onDoubleTap(tap); this.taps = []; return; } this.taps = [tap]; }, // Handle a double tap by zooming in a single zoom level to a // round zoom. onDoubleTap: function(tap) { var z = this.map.getZoom(), // current zoom tz = Math.round(z) + 1, // target zoom dz = tz - z; // desired delate // zoom in to a round number var p = new MM.Point(tap.x, tap.y); this.map.zoomByAbout(dz, p); }, // Re-transform the actual map parent's CSS transformation onPanning: function(touch) { var pos = { x: touch.screenX, y: touch.screenY }, prev = this.locations[touch.identifier]; this.map.panBy(pos.x - prev.x, pos.y - prev.y); }, onPinching: function(e) { // use the first two touches and their previous positions var t0 = e.touches[0], t1 = e.touches[1], p0 = new MM.Point(t0.screenX, t0.screenY), p1 = new MM.Point(t1.screenX, t1.screenY), l0 = this.locations[t0.identifier], l1 = this.locations[t1.identifier]; // mark these touches so they aren't used as taps/holds l0.wasPinch = true; l1.wasPinch = true; // scale about the center of these touches var center = MM.Point.interpolate(p0, p1, 0.5); this.map.zoomByAbout( Math.log(e.scale) / Math.LN2 - Math.log(l0.scale) / Math.LN2, center ); // pan from the previous center of these touches var prevCenter = MM.Point.interpolate(l0, l1, 0.5); this.map.panBy(center.x - prevCenter.x, center.y - prevCenter.y); this.wasPinching = true; this.lastPinchCenter = center; }, // When a pinch event ends, round the zoom of the map. onPinched: function(p) { // TODO: easing if (this.options.snapToZoom) { var z = this.map.getZoom(), // current zoom tz = Math.round(z); // target zoom this.map.zoomByAbout(tz - z, p); } this.wasPinching = false; } }; stamen-modestmaps-js-52d7710/src/start.js0000664000175000017500000000071511653546654017306 0ustar daviddavid/*! * Modest Maps JS v0.21.0 * http://modestmaps.com/ * * Copyright (c) 2011 Stamen Design, All Rights Reserved. * * Open source under the BSD License. * http://creativecommons.org/licenses/BSD/ * * Versioned using Semantic Versioning (v.major.minor.patch) * See CHANGELOG and http://semver.org/ for more details. * */ // namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = {}; } } (function(MM) { stamen-modestmaps-js-52d7710/src/end.js0000664000175000017500000000075711653546654016725 0ustar daviddavid if (typeof module !== 'undefined' && module.exports) { module.exports = { Point: MM.Point, Projection: MM.Projection, MercatorProjection: MM.MercatorProjection, LinearProjection: MM.LinearProjection, Transformation: MM.Transformation, Location: MM.Location, MapProvider: MM.MapProvider, TemplatedMapProvider: MM.TemplatedMapProvider, Coordinate: MM.Coordinate }; } })(com.modestmaps); stamen-modestmaps-js-52d7710/src/callbacks.js0000664000175000017500000000430011653546654020062 0ustar daviddavid // CallbackManager // --------------- // A general-purpose event binding manager used by `Map` // and `RequestManager` // Construct a new CallbackManager, with an list of // supported events. MM.CallbackManager = function(owner, events) { this.owner = owner; this.callbacks = {}; for (var i = 0; i < events.length; i++) { this.callbacks[events[i]] = []; } }; // CallbackManager does simple event management for modestmaps MM.CallbackManager.prototype = { // The element on which callbacks will be triggered. owner: null, // An object of callbacks in the form // // { event: function } callbacks: null, // Add a callback to this object - where the `event` is a string of // the event name and `callback` is a function. addCallback: function(event, callback) { if (typeof(callback) == 'function' && this.callbacks[event]) { this.callbacks[event].push(callback); } }, // Remove a callback. The given function needs to be equal (`===`) to // the callback added in `addCallback`, so named functions should be // used as callbacks. removeCallback: function(event, callback) { if (typeof(callback) == 'function' && this.callbacks[event]) { var cbs = this.callbacks[event], len = cbs.length; for (var i = 0; i < len; i++) { if (cbs[i] === callback) { cbs.splice(i,1); break; } } } }, // Trigger a callback, passing it an object or string from the second // argument. dispatchCallback: function(event, message) { if(this.callbacks[event]) { for (var i = 0; i < this.callbacks[event].length; i += 1) { try { this.callbacks[event][i](this.owner, message); } catch(e) { //console.log(e); // meh } } } } }; stamen-modestmaps-js-52d7710/src/map.js0000664000175000017500000007716611653546654016744 0ustar daviddavid // Map // Instance of a map intended for drawing to a div. // // * `parent` (required DOM element) // Can also be an ID of a DOM element // * `provider` (required MapProvider) // Provides tile URLs and map projections // * `dimensions` (optional Point) // Size of map to create // * `eventHandlers` (optional Array) // If empty or null MouseHandler will be used // Otherwise, each handler will be called with init(map) MM.Map = function(parent, provider, dimensions, eventHandlers) { if (typeof parent == 'string') { parent = document.getElementById(parent); if (!parent) { throw 'The ID provided to modest maps could not be found.'; } } this.parent = parent; // we're no longer adding width and height to parent.style but we still // need to enforce padding, overflow and position otherwise everything screws up // TODO: maybe console.warn if the current values are bad? this.parent.style.padding = '0'; this.parent.style.overflow = 'hidden'; var position = MM.getStyle(this.parent, 'position'); if (position != 'relative' && position != 'absolute') { this.parent.style.position = 'relative'; } // if you don't specify dimensions we assume you want to fill the parent // unless the parent has no w/h, in which case we'll still use a default if (!dimensions) { dimensions = new MM.Point( this.parent.offsetWidth, this.parent.offsetHeight); this.autoSize = true; // FIXME: listeners like this will stop the map being removed cleanly? // when does removeEvent get called? var theMap = this; MM.addEvent(window, 'resize', this.windowResize()); } else { this.autoSize = false; this.parent.style.width = Math.round(dimensions.x) + 'px'; this.parent.style.height = Math.round(dimensions.y) + 'px'; } this.dimensions = dimensions; this.requestManager = new MM.RequestManager(this.parent); this.requestManager.addCallback('requestcomplete', this.getTileComplete()); this.layers = {}; this.layerParent = document.createElement('div'); this.layerParent.id = this.parent.id + '-layers'; // this text is also used in createOrGetLayer this.layerParent.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0'; this.parent.appendChild(this.layerParent); this.coordinate = new MM.Coordinate(0.5, 0.5, 0); this.setProvider(provider); this.enablePyramidLoading = false; this.callbackManager = new MM.CallbackManager(this, [ 'zoomed', 'panned', 'centered', 'extentset', 'resized', 'drawn' ]); // set up handlers last so that all required attributes/functions are in place if needed if (eventHandlers === undefined) { this.eventHandlers = []; this.eventHandlers.push(new MM.MouseHandler(this)); } else { this.eventHandlers = eventHandlers; if (eventHandlers instanceof Array) { for (var i = 0; i < eventHandlers.length; i++) { eventHandlers[i].init(this); } } } }; MM.Map.prototype = { parent: null, provider: null, dimensions: null, coordinate: null, tiles: null, layers: null, layerParent: null, requestManager: null, tileCacheSize: null, maxTileCacheSize: null, recentTiles: null, recentTilesById: null, recentTileSize: null, callbackManager: null, eventHandlers: null, autoSize: null, toString: function() { return 'Map(#' + this.parent.id + ')'; }, // callbacks... addCallback: function(event, callback) { this.callbackManager.addCallback(event, callback); return this; }, removeCallback: function(event, callback) { this.callbackManager.removeCallback(event, callback); return this; }, dispatchCallback: function(event, message) { this.callbackManager.dispatchCallback(event, message); return this; }, windowResize: function() { if (!this._windowResize) { var theMap = this; this._windowResize = function(event) { // don't call setSize here because it sets parent.style.width/height // and setting the height breaks percentages and default styles theMap.dimensions = new MM.Point(theMap.parent.offsetWidth, theMap.parent.offsetHeight); theMap.draw(); theMap.dispatchCallback('resized', [theMap.dimensions]); }; } return this._windowResize; }, // zooming zoomBy: function(zoomOffset) { this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); MM.getFrame(this.getRedraw()); this.dispatchCallback('zoomed', zoomOffset); return this; }, zoomIn: function() { return this.zoomBy(1); }, zoomOut: function() { return this.zoomBy(-1); }, setZoom: function(z) { return this.zoomBy(z - this.coordinate.zoom); }, zoomByAbout: function(zoomOffset, point) { var location = this.pointLocation(point); this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); var newPoint = this.locationPoint(location); this.dispatchCallback('zoomed', zoomOffset); return this.panBy(point.x - newPoint.x, point.y - newPoint.y); }, // panning panBy: function(dx, dy) { this.coordinate.column -= dx / this.provider.tileWidth; this.coordinate.row -= dy / this.provider.tileHeight; this.coordinate = this.enforceLimits(this.coordinate); // Defer until the browser is ready to draw. MM.getFrame(this.getRedraw()); this.dispatchCallback('panned', [dx, dy]); return this; }, /* panZoom: function(dx, dy, zoom) { this.coordinate.column -= dx / this.provider.tileWidth; this.coordinate.row -= dy / this.provider.tileHeight; this.coordinate = this.coordinate.zoomTo(zoom); // Defer until the browser is ready to draw. MM.getFrame(this.getRedraw()); this.dispatchCallback('panned', [dx, dy]); return this; }, */ panLeft: function() { return this.panBy(100, 0); }, panRight: function() { return this.panBy(-100, 0); }, panDown: function() { return this.panBy(0, -100); }, panUp: function() { return this.panBy(0, 100); }, // positioning setCenter: function(location) { return this.setCenterZoom(location, this.coordinate.zoom); }, setCenterZoom: function(location, zoom) { this.coordinate = this.provider.locationCoordinate(location).zoomTo(parseFloat(zoom) || 0); this.draw(); this.dispatchCallback('centered', [location, zoom]); return this; }, setExtent: function(locations, any) { var TL, BR; for (var i = 0; i < locations.length; i++) { var coordinate = this.provider.locationCoordinate(locations[i]); if (TL) { TL.row = Math.min(TL.row, coordinate.row); TL.column = Math.min(TL.column, coordinate.column); TL.zoom = Math.min(TL.zoom, coordinate.zoom); BR.row = Math.max(BR.row, coordinate.row); BR.column = Math.max(BR.column, coordinate.column); BR.zoom = Math.max(BR.zoom, coordinate.zoom); } else { TL = coordinate.copy(); BR = coordinate.copy(); } } var width = this.dimensions.x + 1; var height = this.dimensions.y + 1; // multiplication factor between horizontal span and map width var hFactor = (BR.column - TL.column) / (width / this.provider.tileWidth); // multiplication factor expressed as base-2 logarithm, for zoom difference var hZoomDiff = Math.log(hFactor) / Math.log(2); // possible horizontal zoom to fit geographical extent in map width var hPossibleZoom = TL.zoom - (any ? hZoomDiff : Math.ceil(hZoomDiff)); // multiplication factor between vertical span and map height var vFactor = (BR.row - TL.row) / (height / this.provider.tileHeight); // multiplication factor expressed as base-2 logarithm, for zoom difference var vZoomDiff = Math.log(vFactor) / Math.log(2); // possible vertical zoom to fit geographical extent in map height var vPossibleZoom = TL.zoom - (any ? vZoomDiff : Math.ceil(vZoomDiff)); // initial zoom to fit extent vertically and horizontally var initZoom = Math.min(hPossibleZoom, vPossibleZoom); // additionally, make sure it's not outside the boundaries set by provider limits // this also catches Infinity stuff initZoom = Math.min(initZoom, this.provider.outerLimits()[1].zoom); initZoom = Math.max(initZoom, this.provider.outerLimits()[0].zoom); // coordinate of extent center var centerRow = (TL.row + BR.row) / 2; var centerColumn = (TL.column + BR.column) / 2; var centerZoom = TL.zoom; this.coordinate = new MM.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom); this.draw(); // draw calls enforceLimits // (if you switch to getFrame, call enforceLimits first) this.dispatchCallback('extentset', locations); return this; }, // Resize the map's container `
`, redrawing the map and triggering // `resized` to make sure that the map's presentation is still correct. setSize: function(dimensionsOrX, orY) { if (dimensionsOrX.hasOwnProperty('x') && dimensionsOrX.hasOwnProperty('y')) { this.dimensions = dimensionsOrX; } else if (orY !== undefined && !isNaN(orY)) { this.dimensions = new MM.Point(dimensionsOrX, orY); } this.parent.style.width = Math.round(this.dimensions.x) + 'px'; this.parent.style.height = Math.round(this.dimensions.y) + 'px'; this.draw(); this.dispatchCallback('resized', [this.dimensions]); return this; }, // projecting points on and off screen coordinatePoint: function(coord) { // Return an x, y point on the map image for a given coordinate. if (coord.zoom != this.coordinate.zoom) { coord = coord.zoomTo(this.coordinate.zoom); } // distance from the center of the map var point = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2); point.x += this.provider.tileWidth * (coord.column - this.coordinate.column); point.y += this.provider.tileHeight * (coord.row - this.coordinate.row); return point; }, // Get a `MM.Coordinate` from an `MM.Point` - returns a new tile-like object // from a screen point. pointCoordinate: function(point) { // new point coordinate reflecting distance from map center, in tile widths var coord = this.coordinate.copy(); coord.column += (point.x - this.dimensions.x / 2) / this.provider.tileWidth; coord.row += (point.y - this.dimensions.y / 2) / this.provider.tileHeight; return coord; }, // Return an x, y point on the map image for a given geographical location. locationPoint: function(location) { return this.coordinatePoint(this.provider.locationCoordinate(location)); }, // Return a geographical location on the map image for a given x, y point. pointLocation: function(point) { return this.provider.coordinateLocation(this.pointCoordinate(point)); }, // inspecting getExtent: function() { var extent = []; extent.push(this.pointLocation(new MM.Point(0, 0))); extent.push(this.pointLocation(this.dimensions)); return extent; }, // Get the current centerpoint of the map, returning a `Location` getCenter: function() { return this.provider.coordinateLocation(this.coordinate); }, // Get the current zoom level of the map, returning a number getZoom: function() { return this.coordinate.zoom; }, // Replace the existing provider or set a provider on the map, clearing // out existing tiles and requests. setProvider: function(newProvider) { var firstProvider = false; if (this.provider === null) { firstProvider = true; } // if we already have a provider the we'll need to // clear the DOM, cancel requests and redraw if (!firstProvider) { this.requestManager.clear(); for (var name in this.layers) { if (this.layers.hasOwnProperty(name)) { var layer = this.layers[name]; while (layer.firstChild) { layer.removeChild(layer.firstChild); } } } } // first provider or not we'll init/reset some values... this.tiles = {}; this.tileCacheSize = 0; this.maxTileCacheSize = 64; this.recentTiles = []; this.recentTilesById = {}; // for later: check geometry of old provider and set a new coordinate center // if needed (now? or when?) this.provider = newProvider; if (!firstProvider) { this.draw(); } return this; }, // stats /* getStats: function() { return { 'Request Queue Length': this.requestManager.requestQueue.length, 'Open Request Count': this.requestManager.requestCount, 'Tile Cache Size': this.tileCacheSize, 'Tiles On Screen': this.parent.getElementsByTagName('img').length }; },*/ // Prevent the user from navigating the map outside the `outerLimits` // of the map's provider. enforceLimits: function(coord) { coord = coord.copy(); var limits = this.provider.outerLimits(); if (limits) { var minZoom = limits[0].zoom; var maxZoom = limits[1].zoom; if (coord.zoom < minZoom) { coord = coord.zoomTo(minZoom); } else if (coord.zoom > maxZoom) { coord = coord.zoomTo(maxZoom); } } return coord; }, // Redraw the tiles on the map, reusing existing tiles. draw: function() { // make sure we're not too far in or out: this.coordinate = this.enforceLimits(this.coordinate); // if we're in between zoom levels, we need to choose the nearest: var baseZoom = Math.round(this.coordinate.zoom); // if we don't have dimensions, check the parent size if (this.dimensions.x <= 0 || this.dimensions.y <= 0) { if (this.autoSize) { // maybe the parent size has changed? var w = this.parent.offsetWidth, h = this.parent.offsetHeight this.dimensions = new MM.Point(w,h); if (w <= 0 || h <= 0) { return; } } else { // the issue can only be corrected with setSize return; } } // these are the top left and bottom right tile coordinates // we'll be loading everything in between: var startCoord = this.pointCoordinate(new MM.Point(0, 0)).zoomTo(baseZoom).container(); var endCoord = this.pointCoordinate(this.dimensions).zoomTo(baseZoom).container().right().down(); var tilePadding = 0; if (tilePadding) { startCoord = startCoord.left(tilePadding).up(tilePadding); endCoord = endCoord.right(tilePadding).down(tilePadding); } // tiles with invalid keys will be removed from visible layers // requests for tiles with invalid keys will be canceled // (this object maps from a tile key to a boolean) var validTileKeys = { }; // make sure we have a container for tiles in the current layer var thisLayer = this.createOrGetLayer(startCoord.zoom); // use this coordinate for generating keys, parents and children: var tileCoord = startCoord.copy(); for (tileCoord.column = startCoord.column; tileCoord.column <= endCoord.column; tileCoord.column += 1) { for (tileCoord.row = startCoord.row; tileCoord.row <= endCoord.row; tileCoord.row += 1) { var tileKey = tileCoord.toKey(); validTileKeys[tileKey] = true; if (tileKey in this.tiles) { var tile = this.tiles[tileKey]; // ensure it's in the DOM: if (tile.parentNode != thisLayer) { thisLayer.appendChild(tile); } } else { if (!this.requestManager.hasRequest(tileKey)) { var tileURL = this.provider.getTileUrl(tileCoord); this.requestManager.requestTile(tileKey, tileCoord, tileURL); } // look for a parent tile in our image cache var tileCovered = false; var maxStepsOut = tileCoord.zoom; for (var pz = 1; pz <= maxStepsOut; pz++) { var parentCoord = tileCoord.zoomBy(-pz).container(); var parentKey = parentCoord.toKey(); if (this.enablePyramidLoading) { // mark all parent tiles valid validTileKeys[parentKey] = true; var parentLayer = this.createOrGetLayer(parentCoord.zoom); /* parentLayer.coordinate = parentCoord.copy(); */ if (parentKey in this.tiles) { var parentTile = this.tiles[parentKey]; if (parentTile.parentNode != parentLayer) { parentLayer.appendChild(parentTile); } } else if (!this.requestManager.hasRequest(parentKey)) { // force load of parent tiles we don't already have this.requestManager.requestTile(parentKey, parentCoord, this.provider.getTileUrl(parentCoord)); } } else { // only mark it valid if we have it already if (parentKey in this.tiles) { validTileKeys[parentKey] = true; tileCovered = true; break; } } } // if we didn't find a parent, look at the children: if (!tileCovered && !this.enablePyramidLoading) { var childCoord = tileCoord.zoomBy(1); // mark everything valid whether or not we have it: validTileKeys[childCoord.toKey()] = true; childCoord.column += 1; validTileKeys[childCoord.toKey()] = true; childCoord.row += 1; validTileKeys[childCoord.toKey()] = true; childCoord.column -= 1; validTileKeys[childCoord.toKey()] = true; } } } } // i from i to zoom-5 are layers that would be scaled too big, // i from zoom+2 to layers.length are layers that would be // scaled too small (and tiles would be too numerous) for (var name in this.layers) { if (this.layers.hasOwnProperty(name)) { var zoom = parseInt(name, 10); if (zoom >= startCoord.zoom - 5 && zoom < startCoord.zoom + 2) { continue; } var layer = this.layers[name]; layer.style.display = 'none'; var visibleTiles = layer.getElementsByTagName('img'); for (var j = visibleTiles.length - 1; j >= 0; j--) { layer.removeChild(visibleTiles[j]); } } } // for tracking time of tile usage: var now = new Date().getTime(); // layers we want to see, if they have tiles in validTileKeys var minLayer = startCoord.zoom - 5; var maxLayer = startCoord.zoom + 2; for (var i = minLayer; i < maxLayer; i++) { var layer = this.layers[i]; if (!layer) { // no tiles for this layer yet continue; } // getElementsByTagName is x10 faster than childNodes, and // let's reuse the access. var scale = 1, theCoord = this.coordinate.copy(), visibleTiles = layer.getElementsByTagName('img'); if (visibleTiles.length > 0) { layer.style.display = 'block'; scale = Math.pow(2, this.coordinate.zoom - i); theCoord = theCoord.zoomTo(i); } else { layer.style.display = 'none'; } var tileWidth = this.provider.tileWidth * scale, tileHeight = this.provider.tileHeight * scale, center = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2); for (var j = visibleTiles.length - 1; j >= 0; j--) { var tile = visibleTiles[j]; if (!validTileKeys[tile.id]) { layer.removeChild(tile); } else { // position tiles MM.moveElement(tile, { x: Math.round(center.x + (tile.coord.column - theCoord.column) * tileWidth), y: Math.round(center.y + (tile.coord.row - theCoord.row) * tileHeight), scale: scale, width: this.provider.tileWidth, height: this.provider.tileHeight }); // log last-touched-time of currently cached tiles this.recentTilesById[tile.id].lastTouchedTime = now; } } } // cancel requests that aren't visible: this.requestManager.clearExcept(validTileKeys); // get newly requested tiles, sort according to current view: this.requestManager.processQueue(this.getCenterDistanceCompare()); // make sure we don't have too much stuff: this.checkCache(); this.dispatchCallback('drawn'); }, _tileComplete: null, getTileComplete: function() { if (!this._tileComplete) { var theMap = this; this._tileComplete = function(manager, tile) { // cache the tile itself: theMap.tiles[tile.id] = tile; theMap.tileCacheSize++; // also keep a record of when we last touched this tile: var record = { id: tile.id, lastTouchedTime: new Date().getTime() }; theMap.recentTilesById[tile.id] = record; theMap.recentTiles.push(record); var theCoord = theMap.coordinate.zoomTo(tile.coord.zoom); var scale = Math.pow(2, theMap.coordinate.zoom - tile.coord.zoom); var tx = ((theMap.dimensions.x / 2) + (tile.coord.column - theCoord.column) * theMap.provider.tileWidth * scale); var ty = ((theMap.dimensions.y / 2) + (tile.coord.row - theCoord.row) * theMap.provider.tileHeight * scale); MM.moveElement(tile, { x: Math.round(tx), y: Math.round(ty), scale: scale, // TODO: pass only scale or only w/h width: theMap.provider.tileWidth, height: theMap.provider.tileHeight }); // Support style transition if available. // add tile to its layer var theLayer = theMap.layers[tile.coord.zoom]; theLayer.appendChild(tile); tile.className = 'map-tile-loaded'; // ensure the layer is visible if it's still the current layer if (Math.round(theMap.coordinate.zoom) === tile.coord.zoom) { theLayer.style.display = 'block'; } // request a lazy redraw of all layers // this will remove tiles that were only visible // to cover this tile while it loaded: theMap.requestRedraw(); }; } return this._tileComplete; }, _redrawTimer: undefined, requestRedraw: function() { // we'll always draw within 1 second of this request, // sometimes faster if there's already a pending redraw // this is used when a new tile arrives so that we clear // any parent/child tiles that were only being displayed // until the tile loads at the right zoom level if (!this._redrawTimer) { this._redrawTimer = setTimeout(this.getRedraw(), 1000); } }, _redraw: null, getRedraw: function() { // let's only create this closure once... if (!this._redraw) { var theMap = this; this._redraw = function() { theMap.draw(); theMap._redrawTimer = 0; }; } return this._redraw; }, createOrGetLayer: function(zoom) { if (zoom in this.layers) { return this.layers[zoom]; } //console.log('creating layer ' + zoom); var layer = document.createElement('div'); layer.id = this.parent.id + '-zoom-' + zoom; layer.style.cssText = this.layerParent.style.cssText; layer.style.zIndex = zoom; this.layerParent.appendChild(layer); this.layers[zoom] = layer; return layer; }, // keeps cache below max size // (called every time we receive a new tile and add it to the cache) checkCache: function() { var numTilesOnScreen = this.parent.getElementsByTagName('img').length; var maxTiles = Math.max(numTilesOnScreen, this.maxTileCacheSize); if (this.tileCacheSize > maxTiles) { // sort from newest (highest) to oldest (lowest) this.recentTiles.sort(function(t1, t2) { return t2.lastTouchedTime < t1.lastTouchedTime ? -1 : t2.lastTouchedTime > t1.lastTouchedTime ? 1 : 0; }); } while (this.tileCacheSize > maxTiles) { // delete the oldest record var tileRecord = this.recentTiles.pop(); var now = new Date().getTime(); delete this.recentTilesById[tileRecord.id]; /*window.console.log('removing ' + tileRecord.id + ' last seen ' + (now-tileRecord.lastTouchedTime) + 'ms ago'); */ // now actually remove it from the cache... var tile = this.tiles[tileRecord.id]; if (tile.parentNode) { // I'm leaving this uncommented for now but you should never see it: alert("Gah: trying to removing cached tile even though it's still in the DOM"); } else { delete this.tiles[tileRecord.id]; this.tileCacheSize--; } } }, // Compares manhattan distance from center of // requested tiles to current map center // NB:- requested tiles are *popped* from queue, so we do a descending sort getCenterDistanceCompare: function() { var theCoord = this.coordinate.zoomTo(Math.round(this.coordinate.zoom)); return function(r1, r2) { if (r1 && r2) { var c1 = r1.coord; var c2 = r2.coord; if (c1.zoom == c2.zoom) { var ds1 = Math.abs(theCoord.row - c1.row - 0.5) + Math.abs(theCoord.column - c1.column - 0.5); var ds2 = Math.abs(theCoord.row - c2.row - 0.5) + Math.abs(theCoord.column - c2.column - 0.5); return ds1 < ds2 ? 1 : ds1 > ds2 ? -1 : 0; } else { return c1.zoom < c2.zoom ? 1 : c1.zoom > c2.zoom ? -1 : 0; } } return r1 ? 1 : r2 ? -1 : 0; }; }, // Attempts to destroy all attachment a map has to a page // and clear its memory usage. destroy: function() { this.requestManager.clear(); for (var i = 0; i < this.eventHandlers.length; i++) { this.eventHandlers[i].remove(); } this.parent.removeChild(this.layerParent); MM.removeEvent(window, 'resize', this.windowResize()); return this; } }; stamen-modestmaps-js-52d7710/src/coordinate.js0000664000175000017500000000653511653546654020306 0ustar daviddavid // Coordinate // ---------- // An object representing a tile position, at as specified zoom level. // This is not necessarily a precise tile - `row`, `column`, and // `zoom` can be floating-point numbers, and the `container()` function // can be used to find the actual tile that contains the point. MM.Coordinate = function(row, column, zoom) { this.row = row; this.column = column; this.zoom = zoom; }; MM.Coordinate.prototype = { row: 0, column: 0, zoom: 0, toString: function() { return "(" + this.row.toFixed(3) + ", " + this.column.toFixed(3) + " @" + this.zoom.toFixed(3) + ")"; }, // Quickly generate a string representation of this coordinate to // index it in hashes. toKey: function() { // We've tried to use efficient hash functions here before but we took // them out. Contributions welcome but watch out for collisions when the // row or column are negative and check thoroughly (exhaustively) before // committing. return [ this.zoom, this.row, this.column ].join(','); }, // Clone this object. copy: function() { return new MM.Coordinate(this.row, this.column, this.zoom); }, // Get the actual, rounded-number tile that contains this point. container: function() { // using floor here (not parseInt, ~~) because we want -0.56 --> -1 return new MM.Coordinate(Math.floor(this.row), Math.floor(this.column), Math.floor(this.zoom)); }, // Recalculate this Coordinate at a different zoom level and return the // new object. zoomTo: function(destination) { var power = Math.pow(2, destination - this.zoom); return new MM.Coordinate(this.row * power, this.column * power, destination); }, // Recalculate this Coordinate at a different relative zoom level and return the // new object. zoomBy: function(distance) { var power = Math.pow(2, distance); return new MM.Coordinate(this.row * power, this.column * power, this.zoom + distance); }, // Move this coordinate up by `dist` coordinates up: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row - dist, this.column, this.zoom); }, // Move this coordinate right by `dist` coordinates right: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row, this.column + dist, this.zoom); }, // Move this coordinate down by `dist` coordinates down: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row + dist, this.column, this.zoom); }, // Move this coordinate left by `dist` coordinates left: function(dist) { if (dist === undefined) dist = 1; return new MM.Coordinate(this.row, this.column - dist, this.zoom); } }; stamen-modestmaps-js-52d7710/src/mouse.js0000664000175000017500000001242511653546654017302 0ustar daviddavid // Event Handlers // -------------- // A utility function for finding the offset of the // mouse from the top-left of the page MM.getMousePoint = function(e, map) { // start with just the mouse (x, y) var point = new MM.Point(e.clientX, e.clientY); // correct for scrolled document point.x += document.body.scrollLeft + document.documentElement.scrollLeft; point.y += document.body.scrollTop + document.documentElement.scrollTop; // correct for nested offsets in DOM for (var node = map.parent; node; node = node.offsetParent) { point.x -= node.offsetLeft; point.y -= node.offsetTop; } return point; }; // A handler that allows mouse-wheel zooming - zooming in // when page would scroll up, and out when the page would scroll down. MM.MouseWheelHandler = function(map) { if (map !== undefined) this.init(map); }; MM.MouseWheelHandler.prototype = { init: function(map) { this.map = map; this._mouseWheel = MM.bind(this.mouseWheel, this); MM.addEvent(map.parent, 'mousewheel', this._mouseWheel); }, remove: function() { MM.removeEvent(this.map.parent, 'mousewheel', this._mouseWheel); }, mouseWheel: function(e) { var delta = 0; this.prevTime = this.prevTime || new Date().getTime(); if (e.wheelDelta) { delta = e.wheelDelta; } else if (e.detail) { delta = -e.detail; } // limit mousewheeling to once every 200ms var timeSince = new Date().getTime() - this.prevTime; if (Math.abs(delta) > 0 && (timeSince > 200)) { var point = MM.getMousePoint(e, this.map); this.map.zoomByAbout(delta > 0 ? 1 : -1, point); this.prevTime = new Date().getTime(); } // Cancel the event so that the page doesn't scroll return MM.cancelEvent(e); } }; // Handle double clicks, that zoom the map in one zoom level. MM.DoubleClickHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.DoubleClickHandler.prototype = { init: function(map) { this.map = map; this._doubleClick = MM.bind(this.doubleClick, this); MM.addEvent(map.parent, 'dblclick', this._doubleClick); }, remove: function() { MM.removeEvent(this.map.parent, 'dblclick', this._doubleClick); }, doubleClick: function(e) { // Ensure that this handler is attached once. // Get the point on the map that was double-clicked var point = MM.getMousePoint(e, this.map); // use shift-double-click to zoom out this.map.zoomByAbout(e.shiftKey ? -1 : 1, point); return MM.cancelEvent(e); } }; // Handle the use of mouse dragging to pan the map. MM.DragHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.DragHandler.prototype = { init: function(map) { this.map = map; this._mouseDown = MM.bind(this.mouseDown, this); MM.addEvent(map.parent, 'mousedown', this._mouseDown); }, remove: function() { MM.removeEvent(this.map.parent, 'mousedown', this._mouseDown); }, mouseDown: function(e) { MM.addEvent(document, 'mouseup', this._mouseUp = MM.bind(this.mouseUp, this)); MM.addEvent(document, 'mousemove', this._mouseMove = MM.bind(this.mouseMove, this)); this.prevMouse = new MM.Point(e.clientX, e.clientY); this.map.parent.style.cursor = 'move'; return MM.cancelEvent(e); }, mouseMove: function(e) { if (this.prevMouse) { this.map.panBy( e.clientX - this.prevMouse.x, e.clientY - this.prevMouse.y); this.prevMouse.x = e.clientX; this.prevMouse.y = e.clientY; this.prevMouse.t = +new Date(); } return MM.cancelEvent(e); }, mouseUp: function(e) { MM.removeEvent(document, 'mouseup', this._mouseUp); MM.removeEvent(document, 'mousemove', this._mouseMove); this.prevMouse = null; this.map.parent.style.cursor = ''; return MM.cancelEvent(e); } }; // A shortcut for adding drag, double click, // and mouse wheel events to the map. This is the default // handler attached to a map if the handlers argument isn't given. MM.MouseHandler = function(map) { if (map !== undefined) { this.init(map); } }; MM.MouseHandler.prototype = { init: function(map) { this.map = map; this.handlers = [ new MM.DragHandler(map), new MM.DoubleClickHandler(map), new MM.MouseWheelHandler(map) ]; }, remove: function() { for (var i = 0; i < this.handlers.length; i++) { this.handlers[i].remove(); } } }; stamen-modestmaps-js-52d7710/src/transformation.js0000664000175000017500000000527411653546654021224 0ustar daviddavid // Transformation // -------------- MM.Transformation = function(ax, bx, cx, ay, by, cy) { this.ax = ax; this.bx = bx; this.cx = cx; this.ay = ay; this.by = by; this.cy = cy; }; MM.Transformation.prototype = { ax: 0, bx: 0, cx: 0, ay: 0, by: 0, cy: 0, transform: function(point) { return new MM.Point(this.ax * point.x + this.bx * point.y + this.cx, this.ay * point.x + this.by * point.y + this.cy); }, untransform: function(point) { return new MM.Point((point.x * this.by - point.y * this.bx - this.cx * this.by + this.cy * this.bx) / (this.ax * this.by - this.ay * this.bx), (point.x * this.ay - point.y * this.ax - this.cx * this.ay + this.cy * this.ax) / (this.bx * this.ay - this.by * this.ax)); } }; // Generates a transform based on three pairs of points, // a1 -> a2, b1 -> b2, c1 -> c2. MM.deriveTransformation = function(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y, c1x, c1y, c2x, c2y) { var x = MM.linearSolution(a1x, a1y, a2x, b1x, b1y, b2x, c1x, c1y, c2x); var y = MM.linearSolution(a1x, a1y, a2y, b1x, b1y, b2y, c1x, c1y, c2y); return new MM.Transformation(x[0], x[1], x[2], y[0], y[1], y[2]); }; // Solves a system of linear equations. // // t1 = (a * r1) + (b + s1) + c // t2 = (a * r2) + (b + s2) + c // t3 = (a * r3) + (b + s3) + c // // r1 - t3 are the known values. // a, b, c are the unknowns to be solved. // returns the a, b, c coefficients. MM.linearSolution = function(r1, s1, t1, r2, s2, t2, r3, s3, t3) { // make them all floats r1 = parseFloat(r1); s1 = parseFloat(s1); t1 = parseFloat(t1); r2 = parseFloat(r2); s2 = parseFloat(s2); t2 = parseFloat(t2); r3 = parseFloat(r3); s3 = parseFloat(s3); t3 = parseFloat(t3); var a = (((t2 - t3) * (s1 - s2)) - ((t1 - t2) * (s2 - s3))) / (((r2 - r3) * (s1 - s2)) - ((r1 - r2) * (s2 - s3))); var b = (((t2 - t3) * (r1 - r2)) - ((t1 - t2) * (r2 - r3))) / (((s2 - s3) * (r1 - r2)) - ((s1 - s2) * (r2 - r3))); var c = t1 - (r1 * a) - (s1 * b); return [ a, b, c ]; }; stamen-modestmaps-js-52d7710/src/location.js0000664000175000017500000000577411653546654017773 0ustar daviddavid // Location // -------- MM.Location = function(lat, lon) { this.lat = parseFloat(lat); this.lon = parseFloat(lon); }; MM.Location.prototype = { lat: 0, lon: 0, toString: function() { return "(" + this.lat.toFixed(3) + ", " + this.lon.toFixed(3) + ")"; } }; // returns approximate distance between start and end locations // // default unit is meters // // you can specify different units by optionally providing the // earth's radius in the units you desire // // Default is 6,378,000 metres, suggested values are: // // * 3963.1 statute miles // * 3443.9 nautical miles // * 6378 km // // see [Formula and code for calculating distance based on two lat/lon locations](http://jan.ucc.nau.edu/~cvm/latlon_formula.html) MM.Location.distance = function(l1, l2, r) { if (!r) { // default to meters r = 6378000; } var deg2rad = Math.PI / 180.0, a1 = l1.lat * deg2rad, b1 = l1.lon * deg2rad, a2 = l2.lat * deg2rad, b2 = l2.lon * deg2rad, c = Math.cos(a1) * Math.cos(b1) * Math.cos(a2) * Math.cos(b2), d = Math.cos(a1) * Math.sin(b1) * Math.cos(a2) * Math.sin(b2), e = Math.sin(a1) * Math.sin(a2); return Math.acos(c + d + e) * r; }; // Interpolates along a great circle, f between 0 and 1 // // * FIXME: could be heavily optimized (lots of trig calls to cache) // * FIXME: could be inmproved for calculating a full path MM.Location.interpolate = function(l1, l2, f) { if (l1.lat === l2.lat && l1.lon === l2.lon) { return new MM.Location(l1.lat, l1.lon); } var deg2rad = Math.PI / 180.0, lat1 = l1.lat * deg2rad, lon1 = l1.lon * deg2rad, lat2 = l2.lat * deg2rad, lon2 = l2.lon * deg2rad; var d = 2 * Math.asin( Math.sqrt( Math.pow(Math.sin((lat1 - lat2) / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin((lon1 - lon2) / 2), 2))); var bearing = Math.atan2( Math.sin(lon1 - lon2) * Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon1 - lon2) ) / -(Math.PI / 180); bearing = bearing < 0 ? 360 + bearing : bearing; var A = Math.sin((1-f)*d)/Math.sin(d); var B = Math.sin(f*d)/Math.sin(d); var x = A * Math.cos(lat1) * Math.cos(lon1) + B * Math.cos(lat2) * Math.cos(lon2); var y = A * Math.cos(lat1) * Math.sin(lon1) + B * Math.cos(lat2) * Math.sin(lon2); var z = A * Math.sin(lat1) + B * Math.sin(lat2); var latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); var lonN = Math.atan2(y,x); return new MM.Location(latN / deg2rad, lonN / deg2rad); }; stamen-modestmaps-js-52d7710/src/point.js0000664000175000017500000000135211653546654017300 0ustar daviddavid // Point MM.Point = function(x, y) { this.x = parseFloat(x); this.y = parseFloat(y); }; MM.Point.prototype = { x: 0, y: 0, toString: function() { return "(" + this.x.toFixed(3) + ", " + this.y.toFixed(3) + ")"; } }; // Get the euclidean distance between two points MM.Point.distance = function(p1, p2) { var dx = (p2.x - p1.x); var dy = (p2.y - p1.y); return Math.sqrt(dx*dx + dy*dy); }; // Get a point between two other points, biased by `t`. MM.Point.interpolate = function(p1, p2, t) { var px = p1.x + (p2.x - p1.x) * t; var py = p1.y + (p2.y - p1.y) * t; return new MM.Point(px, py); }; stamen-modestmaps-js-52d7710/src/utils.js0000664000175000017500000001361311653546654017312 0ustar daviddavid // Make inheritance bearable: clone one level of properties MM.extend = function(child, parent) { for (var property in parent.prototype) { if (typeof child.prototype[property] == "undefined") { child.prototype[property] = parent.prototype[property]; } } return child; }; MM.getFrame = function () { // native animation frames // http://webstuff.nfshost.com/anim-timing/Overview.html // http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // can't apply these directly to MM because Chrome needs window // to own webkitRequestAnimationFrame (for example) // perhaps we should namespace an alias onto window instead? // e.g. window.mmRequestAnimationFrame? return function(callback) { (window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(function () { callback(+new Date()); }, 10); })(callback); }; }(); // Inspired by LeafletJS MM.transformProperty = (function(props) { if (!this.document) return; // node.js safety var style = document.documentElement.style; for (var i = 0; i < props.length; i++) { if (props[i] in style) { return props[i]; } } return false; })(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); MM.matrixString = function(point) { // Make the result of point.scale * point.width a whole number. if (point.scale * point.width % 1) { point.scale += (1 - point.scale * point.width % 1) / point.width; } if (MM._browser.webkit3d) { return 'matrix3d(' + [(point.scale || '1'), '0,0,0,0', (point.scale || '1'), '0,0', '0,0,1,0', (point.x + (((point.width * point.scale) - point.width) / 2)).toFixed(4), (point.y + (((point.height * point.scale) - point.height) / 2)).toFixed(4), 0,1].join(',') + ')'; } else { var unit = (MM.transformProperty == 'MozTransform') ? 'px' : ''; return 'matrix(' + [(point.scale || '1'), 0, 0, (point.scale || '1'), (point.x + (((point.width * point.scale) - point.width) / 2)) + unit, (point.y + (((point.height * point.scale) - point.height) / 2)) + unit ].join(',') + ')'; } }; MM._browser = (function(window) { return { webkit: ('WebKitCSSMatrix' in window), webkit3d: ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()) }; })(this); // use this for node.js global MM.moveElement = function(el, point) { if (MM.transformProperty) { // Optimize for identity transforms, where you don't actually // need to change this element's string. Browsers can optimize for // the .style.left case but not for this CSS case. var ms = MM.matrixString(point); if (el[MM.transformProperty] !== ms) { el.style[MM.transformProperty] = el[MM.transformProperty] = ms; } } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; el.style.width = Math.ceil(point.width * point.scale) + 'px'; el.style.height = Math.ceil(point.height * point.scale) + 'px'; } }; // Events // Cancel an event: prevent it from bubbling MM.cancelEvent = function(e) { // there's more than one way to skin this cat e.cancelBubble = true; e.cancel = true; e.returnValue = false; if (e.stopPropagation) { e.stopPropagation(); } if (e.preventDefault) { e.preventDefault(); } return false; }; // see http://ejohn.org/apps/jselect/event.html for the originals MM.addEvent = function(obj, type, fn) { if (obj.attachEvent) { obj['e'+type+fn] = fn; obj[type+fn] = function(){ obj['e'+type+fn](window.event); }; obj.attachEvent('on'+type, obj[type+fn]); } else { obj.addEventListener(type, fn, false); if (type == 'mousewheel') { obj.addEventListener('DOMMouseScroll', fn, false); } } }; // From underscore.js MM.bind = function(func, obj) { var slice = Array.prototype.slice; var nativeBind = Function.prototype.bind; if (func.bind === nativeBind && nativeBind) { return nativeBind.apply(func, slice.call(arguments, 1)); } var args = slice.call(arguments, 2); return function() { return func.apply(obj, args.concat(slice.call(arguments))); }; }; MM.removeEvent = function( obj, type, fn ) { if ( obj.detachEvent ) { obj.detachEvent('on'+type, obj[type+fn]); obj[type+fn] = null; } else { obj.removeEventListener(type, fn, false); if (type == 'mousewheel') { obj.removeEventListener('DOMMouseScroll', fn, false); } } }; // Cross-browser function to get current element style property MM.getStyle = function(el,styleProp) { if (el.currentStyle) return el.currentStyle[styleProp]; else if (window.getComputedStyle) return document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); }; stamen-modestmaps-js-52d7710/src/requests.js0000664000175000017500000002312011653546654020017 0ustar daviddavid // RequestManager // -------------- // an image loading queue MM.RequestManager = function() { // The loading bay is a document fragment to optimize appending, since // the elements within are invisible. See // [this blog post](http://ejohn.org/blog/dom-documentfragments/). this.loadingBay = document.createDocumentFragment(); this.requestsById = {}; this.openRequestCount = 0; this.maxOpenRequests = 4; this.requestQueue = []; this.callbackManager = new MM.CallbackManager(this, ['requestcomplete']); }; MM.RequestManager.prototype = { // DOM element, hidden, for making sure images dispatch complete events loadingBay: null, // all known requests, by ID requestsById: null, // current pending requests requestQueue: null, // current open requests (children of loadingBay) openRequestCount: null, // the number of open requests permitted at one time, clamped down // because of domain-connection limits. maxOpenRequests: null, // for dispatching 'requestcomplete' callbackManager: null, addCallback: function(event, callback) { this.callbackManager.addCallback(event,callback); }, removeCallback: function(event, callback) { this.callbackManager.removeCallback(event,callback); }, dispatchCallback: function(event, message) { this.callbackManager.dispatchCallback(event,message); }, // Clear everything in the queue by excluding nothing clear: function() { this.clearExcept({}); }, // Clear everything in the queue except for certain ids, speciied // by an object of the form // // { id: throwawayvalue } clearExcept: function(validIds) { // clear things from the queue first... for (var i = 0; i < this.requestQueue.length; i++) { var request = this.requestQueue[i]; if (request && !(request.id in validIds)) { this.requestQueue[i] = null; } } // then check the loadingBay... var openRequests = this.loadingBay.childNodes; for (var j = openRequests.length-1; j >= 0; j--) { var img = openRequests[j]; if (!(img.id in validIds)) { this.loadingBay.removeChild(img); this.openRequestCount--; /* console.log(this.openRequestCount + " open requests"); */ img.src = img.coord = img.onload = img.onerror = null; } } // hasOwnProperty protects against prototype additions // > "The standard describes an augmentable Object.prototype. // Ignore standards at your own peril." // -- http://www.yuiblog.com/blog/2006/09/26/for-in-intrigue/ for (var id in this.requestsById) { if (this.requestsById.hasOwnProperty(id)) { if (!(id in validIds)) { var requestToRemove = this.requestsById[id]; // whether we've done the request or not... delete this.requestsById[id]; if (requestToRemove !== null) { requestToRemove = requestToRemove.id = requestToRemove.coord = requestToRemove.url = null; } } } } }, // Given a tile id, check whether the RequestManager is currently // requesting it and waiting for the result. hasRequest: function(id) { return (id in this.requestsById); }, // * TODO: remove dependency on coord (it's for sorting, maybe call it data?) // * TODO: rename to requestImage once it's not tile specific requestTile: function(id, coord, url) { if (!(id in this.requestsById)) { var request = { id: id, coord: coord.copy(), url: url }; // if there's no url just make sure we don't request this image again this.requestsById[id] = request; if (url) { this.requestQueue.push(request); /* console.log(this.requestQueue.length + ' pending requests'); */ } } }, getProcessQueue: function() { // let's only create this closure once... if (!this._processQueue) { var theManager = this; this._processQueue = function() { theManager.processQueue(); }; } return this._processQueue; }, // Select images from the `requestQueue` and create image elements for // them, attaching their load events to the function returned by // `this.getLoadComplete()` so that they can be added to the map. processQueue: function(sortFunc) { // When the request queue fills up beyond 8, start sorting the // requests so that spiral-loading or another pattern can be used. if (sortFunc && this.requestQueue.length > 8) { this.requestQueue.sort(sortFunc); } while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) { var request = this.requestQueue.pop(); if (request) { this.openRequestCount++; /* console.log(this.openRequestCount + ' open requests'); */ // JSLitmus benchmark shows createElement is a little faster than // new Image() in Firefox and roughly the same in Safari: // http://tinyurl.com/y9wz2jj http://tinyurl.com/yes6rrt var img = document.createElement('img'); // FIXME: id is technically not unique in document if there // are two Maps but toKey is supposed to be fast so we're trying // to avoid a prefix ... hence we can't use any calls to // `document.getElementById()` to retrieve images img.id = request.id; img.style.position = 'absolute'; // * FIXME: store this elsewhere to avoid scary memory leaks? // * FIXME: call this 'data' not 'coord' so that RequestManager is less Tile-centric? img.coord = request.coord; // add it to the DOM in a hidden layer, this is a bit of a hack, but it's // so that the event we get in image.onload has srcElement assigned in IE6 this.loadingBay.appendChild(img); // set these before img.src to avoid missing an img that's already cached img.onload = img.onerror = this.getLoadComplete(); img.src = request.url; // keep things tidy request = request.id = request.coord = request.url = null; } } }, _loadComplete: null, // Get the singleton `_loadComplete` function that is called on image // load events, either removing them from the queue and dispatching an // event to add them to the map, or deleting them if the image failed // to load. getLoadComplete: function() { // let's only create this closure once... if (!this._loadComplete) { var theManager = this; this._loadComplete = function(e) { // this is needed because we don't use MM.addEvent for images e = e || window.event; // srcElement for IE, target for FF, Safari etc. var img = e.srcElement || e.target; // unset these straight away so we don't call this twice img.onload = img.onerror = null; // pull it back out of the (hidden) DOM // so that draw will add it correctly later theManager.loadingBay.removeChild(img); theManager.openRequestCount--; delete theManager.requestsById[img.id]; /* console.log(theManager.openRequestCount + ' open requests'); */ // NB:- complete is also true onerror if we got a 404 if (e.type === 'load' && (img.complete || (img.readyState && img.readyState == 'complete'))) { theManager.dispatchCallback('requestcomplete', img); } else { // if it didn't finish clear its src to make sure it // really stops loading // FIXME: we'll never retry because this id is still // in requestsById - is that right? img.src = null; } // keep going in the same order // use `setTimeout()` to avoid the IE recursion limit, see // http://cappuccino.org/discuss/2010/03/01/internet-explorer-global-variables-and-stack-overflows/ // and https://github.com/stamen/modestmaps-js/issues/12 setTimeout(theManager.getProcessQueue(), 0); }; } return this._loadComplete; } }; stamen-modestmaps-js-52d7710/src/projection.js0000664000175000017500000000565311653546654020333 0ustar daviddavid // Projection // ---------- // An abstract class / interface for projections MM.Projection = function(zoom, transformation) { if (!transformation) { transformation = new MM.Transformation(1, 0, 0, 0, 1, 0); } this.zoom = zoom; this.transformation = transformation; }; MM.Projection.prototype = { zoom: 0, transformation: null, rawProject: function(point) { throw "Abstract method not implemented by subclass."; }, rawUnproject: function(point) { throw "Abstract method not implemented by subclass."; }, project: function(point) { point = this.rawProject(point); if(this.transformation) { point = this.transformation.transform(point); } return point; }, unproject: function(point) { if(this.transformation) { point = this.transformation.untransform(point); } point = this.rawUnproject(point); return point; }, locationCoordinate: function(location) { var point = new MM.Point(Math.PI * location.lon / 180.0, Math.PI * location.lat / 180.0); point = this.project(point); return new MM.Coordinate(point.y, point.x, this.zoom); }, coordinateLocation: function(coordinate) { coordinate = coordinate.zoomTo(this.zoom); var point = new MM.Point(coordinate.column, coordinate.row); point = this.unproject(point); return new MM.Location(180.0 * point.y / Math.PI, 180.0 * point.x / Math.PI); } }; // A projection for equilateral maps, based on longitude and latitude MM.LinearProjection = function(zoom, transformation) { MM.Projection.call(this, zoom, transformation); }; // The Linear projection doesn't reproject points MM.LinearProjection.prototype = { rawProject: function(point) { return new MM.Point(point.x, point.y); }, rawUnproject: function(point) { return new MM.Point(point.x, point.y); } }; MM.extend(MM.LinearProjection, MM.Projection); MM.MercatorProjection = function(zoom, transformation) { // super! MM.Projection.call(this, zoom, transformation); }; // Project lon/lat points into meters required for Mercator MM.MercatorProjection.prototype = { rawProject: function(point) { return new MM.Point(point.x, Math.log(Math.tan(0.25 * Math.PI + 0.5 * point.y))); }, rawUnproject: function(point) { return new MM.Point(point.x, 2 * Math.atan(Math.pow(Math.E, point.y)) - 0.5 * Math.PI); } }; MM.extend(MM.MercatorProjection, MM.Projection); stamen-modestmaps-js-52d7710/test/0000775000175000017500000000000011653546654016000 5ustar daviddavidstamen-modestmaps-js-52d7710/test/location.test.js0000664000175000017500000000073011653546654021124 0ustar daviddavidvar path = require('path'), sys = require('sys'), assert = require('assert'), MM = require('../modestmaps.js'); exports['basic location'] = function() { var l = new MM.Location(0, 0); assert.deepEqual(l, { lat: 0, lon: 0 }); }; exports['interpolate same point'] = function() { var l1 = new MM.Location(0, 0), l2 = new MM.Location(0, 0), l3 = MM.Location.interpolate(l1, l2, 0.5); assert.deepEqual(l3, { lat: 0, lon: 0 }); }; stamen-modestmaps-js-52d7710/test/browser/0000775000175000017500000000000011653546654017463 5ustar daviddavidstamen-modestmaps-js-52d7710/test/browser/spec/0000775000175000017500000000000011653546654020415 5ustar daviddavidstamen-modestmaps-js-52d7710/test/browser/spec/DragHandler.js0000664000175000017500000000243511653546654023132 0ustar daviddaviddescribe('DragHandler', function() { var map, mm = com.modestmaps; beforeEach(function() { div = document.createElement('div'); div.id = +new Date(); div.style.width = 500; div.style.height = 500; var template = 'http://{S}tile.openstreetmap.org/{Z}/{X}/{Y}.png'; var subdomains = [ '', 'a.', 'b.', 'c.' ]; var provider = new com.modestmaps.TemplatedMapProvider(template, subdomains); map = new com.modestmaps.Map(div, provider, [ new mm.DragHandler() ]); map.setCenterZoom(new com.modestmaps.Location(0, 0), 0); }); it('changes the cursor style to move while moving', function() { happen.mousedown(map.parent, { clientX: 10, clientY: 10 }); expect(map.parent.style.cursor).toEqual('move'); }); it('pan the map when you do a panning motion', function() { expect(~~map.getCenter().lat).toEqual(0); expect(~~map.getCenter().lon).toEqual(0); happen.mousedown(map.parent, { clientX: 10, clientY: 10 }); happen.mousemove(document, { clientX: 30, clientY: 30 }); happen.mouseup(document, { clientX: 30, clientY: 30 }); expect(~~map.getCenter().lat).toEqual(27); expect(~~map.getCenter().lon).toEqual(-28); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Projection.js0000664000175000017500000000230411653546654023066 0ustar daviddaviddescribe('Projection', function() { var MM = com.modestmaps, m; beforeEach(function() { m = new MM.MercatorProjection(10); }); it('can instantiate a mercator projection', function() { // TODO: row is a very small number because of odd javascript math. expect(m.locationCoordinate(new MM.Location(0, 0)).column).toEqual(0); expect(m.locationCoordinate(new MM.Location(0, 0)).zoom).toEqual(10); expect(m.coordinateLocation(new MM.Coordinate(0, 0, 10))).toEqual({ lon: 0, lat: 0 }); }); it('is accurate up to 3 decimals', function() { // Confirm that these values are valid up to a 3 decimals var c2 = m.locationCoordinate(new MM.Location(37, -122)); expect(Math.round(c2.row * 1000) / 1000).toEqual(0.696); expect(Math.round(c2.column * 1000) / 1000).toEqual(-2.129); expect(c2.zoom).toEqual(10); }); it('coordinatelocation to work', function() { var l2 = m.coordinateLocation(new MM.Coordinate(0.696, -2.129, 10)); expect(Math.round(l2.lat * 1000) / 1000).toEqual(37.001); expect(Math.round(l2.lon * 1000) / 1000).toEqual(-121.983); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Map.js0000664000175000017500000000471311653546654021475 0ustar daviddaviddescribe('Map', function() { var map, div, sink; function Receiver() { } Receiver.prototype.receive = function() { }; beforeEach(function() { sink = new Receiver(); div = document.createElement('div'); div.id = +new Date(); div.style.width = 500; div.style.height = 500; var template = 'http://{S}tile.openstreetmap.org/{Z}/{X}/{Y}.png'; var subdomains = [ '', 'a.', 'b.', 'c.' ]; var provider = new com.modestmaps.TemplatedMapProvider(template, subdomains); map = new com.modestmaps.Map(div, provider, new com.modestmaps.Point(400, 400)); map.setCenterZoom(new com.modestmaps.Location(0, 0), 0); }); it('attaches itself to a parent div', function() { expect(map.parent).toEqual(div); }); it('has set a proper zoom level', function() { expect(map.getZoom()).toEqual(0); }); it('has a center coordinate', function() { expect(typeof map.coordinate.row).toEqual('number'); expect(typeof map.coordinate.column).toEqual('number'); expect(typeof map.coordinate.zoom).toEqual('number'); }); it('binds and calls drawn', function() { spyOn(sink, 'receive'); map.addCallback('drawn', sink.receive); runs(function() { map.draw(); }); waits(500); runs(function() { expect(sink.receive).toHaveBeenCalledWith(map, undefined); }); }); it('binds and calls zoomed', function() { spyOn(sink, 'receive'); map.addCallback('zoomed', sink.receive); runs(function() { map.zoomIn(); }); waits(500); runs(function() { expect(sink.receive).toHaveBeenCalledWith(map, 1); }); }); it('binds and calls panned', function() { spyOn(sink, 'receive'); map.addCallback('panned', sink.receive); runs(function() { map.panBy(2, 2); }); waits(500); runs(function() { expect(sink.receive).toHaveBeenCalledWith(map, [2, 2]); }); }); it('binds and calls resized', function() { spyOn(sink, 'receive'); map.addCallback('resized', sink.receive); runs(function() { map.setSize({ x: 200, y: 300 }); }); waits(500); runs(function() { expect(sink.receive).toHaveBeenCalledWith(map, [{ x: 200, y: 300}]); }); }); it('can be cleanly destroyed', function() { map.destroy(); expect(map.requestManager.openRequestCount).toEqual(0); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Point.js0000664000175000017500000000027711653546654022052 0ustar daviddaviddescribe('Point', function() { var MM = com.modestmaps; it('creates a point', function() { var p = new MM.Point(0, 1); expect(p).toEqual({ x: 0, y: 1 }); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Coordinate.js0000664000175000017500000000075011653546654023044 0ustar daviddaviddescribe('Coordinate', function() { var coordinate; beforeEach(function() { coordinate = new com.modestmaps.Coordinate(0, 0, 2); }); it('generates a key', function() { expect(typeof coordinate.toKey()).toEqual('string'); }); it('can provide a zoomed-in coordinate', function() { expect((coordinate.zoomBy(1)).zoom).toEqual(3); }); it('can provide a zoomed-out coordinate', function() { expect((coordinate.zoomBy(-1)).zoom).toEqual(1); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Transformation.js0000664000175000017500000000217111653546654023762 0ustar daviddaviddescribe('Transformation', function() { var MM = com.modestmaps; it('can do an identity transform', function() { var t = new MM.Transformation(1, 0, 0, 0, 1, 0); var p = new MM.Point(1, 1); var p_ = t.transform(p); var p__ = t.untransform(p_); expect(p).toEqual({ x: 1, y: 1 }); expect(p_).toEqual({ x: 1, y: 1 }); expect(p__).toEqual({ x: 1, y: 1 }); }); it('can do an inverse transform', function() { var t = new MM.Transformation(0, 1, 0, 1, 0, 0); var p = new MM.Point(0, 1); var p_ = t.transform(p); var p__ = t.untransform(p_); expect(p).toEqual({ x: 0, y: 1 }); expect(p_).toEqual({ x: 1, y: 0 }); expect(p__).toEqual({ x: 0, y: 1 }); }); it('can do an addition transform', function() { var t = new MM.Transformation(1, 0, 1, 0, 1, 1); var p = new MM.Point(0, 0); var p_ = t.transform(p); var p__ = t.untransform(p_); expect(p).toEqual({ x: 0, y: 0 }); expect(p_).toEqual({ x: 1, y: 1 }); expect(p__).toEqual({ x: 0, y: 0 }); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/DoubleClickHandler.js0000664000175000017500000000230511653546654024431 0ustar daviddaviddescribe('DoubleClickHandler', function() { var map, mm = com.modestmaps; beforeEach(function() { div = document.createElement('div'); div.id = +new Date(); div.style.width = 500; div.style.height = 500; var template = 'http://{S}tile.openstreetmap.org/{Z}/{X}/{Y}.png'; var subdomains = [ '', 'a.', 'b.', 'c.' ]; var provider = new com.modestmaps.TemplatedMapProvider(template, subdomains); map = new com.modestmaps.Map(div, provider, [ new mm.DoubleClickHandler() ]); map.setCenterZoom(new com.modestmaps.Location(0, 0), 0); }); it('does not zoom in on single click', function() { expect(map.getZoom()).toEqual(0); happen.click(map.parent); expect(map.getZoom()).toEqual(0); }); it('zooms in on double click', function() { expect(map.getZoom()).toEqual(0); happen.dblclick(map.parent); expect(map.getZoom()).toEqual(1); }); it('zooms out on double click with shift', function() { map.setZoom(1); happen.dblclick(map.parent, { shift: true }); expect(map.getZoom()).toEqual(0); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/Provider.js0000664000175000017500000000077411653546654022555 0ustar daviddaviddescribe('Providers', function() { var MM = com.modestmaps; // Currently not testing subdomain-based templatedmapprovider, since // the implementation should be kind of undefined. it('basic templatedmapprovider', function() { var p = new MM.TemplatedMapProvider( 'http://{S}.tile.openstreetmap.org/{Z}/{X}/{Y}.png', ['a']); expect(p.getTileUrl(new MM.Coordinate(1225, 1832, 12))).toEqual( 'http://a.tile.openstreetmap.org/12/1832/1225.png'); }); }); stamen-modestmaps-js-52d7710/test/browser/spec/MouseWheelHandler.js0000664000175000017500000000206311653546654024327 0ustar daviddaviddescribe('MouseWheelHandler', function() { var map, mm = com.modestmaps; beforeEach(function() { div = document.createElement('div'); div.id = +new Date(); div.style.width = 500; div.style.height = 500; var template = 'http://{S}tile.openstreetmap.org/{Z}/{X}/{Y}.png'; var subdomains = [ '', 'a.', 'b.', 'c.' ]; var provider = new com.modestmaps.TemplatedMapProvider(template, subdomains); map = new com.modestmaps.Map(div, provider, [ new mm.MouseWheelHandler() ]); map.setCenterZoom(new com.modestmaps.Location(0, 0), 0); }); it('zooms in the map', function() { runs(function() { happen.once(map.parent, { type: 'mousewheel', detail: -100 }); }); waits(300); runs(function() { happen.once(map.parent, { type: 'mousewheel', detail: -200 }); expect(map.getZoom()).toEqual(1); }); }); }); stamen-modestmaps-js-52d7710/test/browser/lib/0000775000175000017500000000000011653546654020231 5ustar daviddavidstamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/0000775000175000017500000000000011653546654022776 5ustar daviddavidstamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/jasmine.css0000664000175000017500000000411011653546654025132 0ustar daviddavidbody { font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; } .jasmine_reporter a:visited, .jasmine_reporter a { color: #303; } .jasmine_reporter a:hover, .jasmine_reporter a:active { color: blue; } .run_spec { float:right; padding-right: 5px; font-size: .8em; text-decoration: none; } .jasmine_reporter { margin: 0 5px; } .banner { color: #303; background-color: #fef; padding: 5px; } .logo { float: left; font-size: 1.1em; padding-left: 5px; } .logo .version { font-size: .6em; padding-left: 1em; } .runner.running { background-color: yellow; } .options { text-align: right; font-size: .8em; } .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } .suite .suite { margin: 5px; } .suite.passed { background-color: #dfd; } .suite.failed { background-color: #fdd; } .spec { margin: 5px; padding-left: 1em; clear: both; } .spec.failed, .spec.passed, .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } .spec.failed { background-color: #fbb; border-color: red; } .spec.passed { background-color: #bfb; border-color: green; } .spec.skipped { background-color: #bbb; } .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } .passed { background-color: #cfc; display: none; } .failed { background-color: #fbb; } .skipped { color: #777; background-color: #eee; display: none; } /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ .resultMessage span.result { display: block; line-height: 2em; color: black; } .resultMessage .mismatch { color: black; } .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } .finished-at { padding-left: 1em; font-size: .6em; } .show-passed .passed, .show-skipped .skipped { display: block; } #jasmine_content { position:fixed; right: 100%; } .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } stamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/jasmine_favicon.png0000664000175000017500000000161111653546654026636 0ustar daviddavid‰PNG  IHDRóÿagAMA± üasRGB®Îé cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs  šœàIDAT8Ë¥’KHTa†ßÿœÿœOsó8K¢T´Èñ’—Œ2‹"ÒZA-„îEЦM‹Ѧ-¢"¢ EQÑ= £Ò-™2GÇrÆ2ÓÏèèœÿoQR¸ˆžÕûÁ÷½|7‚_LN·O_¿©fw{[‡çþ½º‹Zld ²”2¿¢lÅ´¬Ì®ë‰±øÕ+·Oj±áÆ“ïέ|íkÔÚ>½;±ïFnδ"‰ŠÒÖmëöz?4E‚]üÓ`€_¿sÑk³šœÉ:š…ÅóÒœN#•$a͆u+§ggeߺzýö¶;¶Û]éfÆDQ„ß×ÑjýŒÆG5Îá8c(«˜ïΘšá6[,àœƒ‚ØP4ñôɳ:ã‰dµë¯ÕÞ¹_/B@1™ÁÁœ347¾ìxÕà©?º˜±ØH´ýÝû·³‹ó—ÙTÕÆ0†÷‘'øm‚Ev"Ö@)5•–¦ZM®` Û?<×~8êÔŠåË‹JпɒdaTG$Ñ‹–ÐCXdº ¡:†Ü¼Y™•K.irà Ï#j2)jUõâšµ›×n),+É7Sc $9©=˜bšñs z"Ù`Ä’ê¥UçÏ^:*Î)+X¾kÏ®ƒÅs˳ˆH ç‚ €é:]B¶½2SÐÿ5ÄeƒL(¥e’bõ65·Š}}ßo<͇]Ír¹œéœ1êíøàóEl©ªÉ¢€RŠ`ggèøþ#Ÿ74µ¶xßµ¶x[üím$¹ƒ4ÕêÊsÏ,W”¥ëc°Ó]0«òð©c‡¨$AÐå÷‡VU­®üò%ä…ßî쫯o¼™ŒÍ³MŠÛÔ4#®ëœ `‚îöÏî‘Rè‰1ÔÞ}p/þø3Ndð-<ÐãixõRµ§Ù.œ>wæüÙËGâ£cÃøJKòWW/ÚHÈÄþ7?gÛ)&ö6<¡IEND®B`‚stamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/MIT.LICENSE0000664000175000017500000000204511653546654024434 0ustar daviddavidCopyright (c) 2008-2011 Pivotal Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. stamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/jasmine.js0000664000175000017500000020224111653546654024763 0ustar daviddavidvar isCommonJS = typeof window == "undefined"; /** * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. * * @namespace */ var jasmine = {}; if (isCommonJS) exports.jasmine = jasmine; /** * @private */ jasmine.unimplementedMethod_ = function() { throw new Error("unimplemented method"); }; /** * Use jasmine.undefined instead of undefined, since undefined is just * a plain old variable and may be redefined by somebody else. * * @private */ jasmine.undefined = jasmine.___undefined___; /** * Show diagnostic messages in the console if set to true * */ jasmine.VERBOSE = false; /** * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. * */ jasmine.DEFAULT_UPDATE_INTERVAL = 250; /** * Default timeout interval in milliseconds for waitsFor() blocks. */ jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; jasmine.getGlobal = function() { function getGlobal() { return this; } return getGlobal(); }; /** * Allows for bound functions to be compared. Internal use only. * * @ignore * @private * @param base {Object} bound 'this' for the function * @param name {Function} function to find */ jasmine.bindOriginal_ = function(base, name) { var original = base[name]; if (original.apply) { return function() { return original.apply(base, arguments); }; } else { // IE support return jasmine.getGlobal()[name]; } }; jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); jasmine.MessageResult = function(values) { this.type = 'log'; this.values = values; this.trace = new Error(); // todo: test better }; jasmine.MessageResult.prototype.toString = function() { var text = ""; for (var i = 0; i < this.values.length; i++) { if (i > 0) text += " "; if (jasmine.isString_(this.values[i])) { text += this.values[i]; } else { text += jasmine.pp(this.values[i]); } } return text; }; jasmine.ExpectationResult = function(params) { this.type = 'expect'; this.matcherName = params.matcherName; this.passed_ = params.passed; this.expected = params.expected; this.actual = params.actual; this.message = this.passed_ ? 'Passed.' : params.message; var trace = (params.trace || new Error(this.message)); this.trace = this.passed_ ? '' : trace; }; jasmine.ExpectationResult.prototype.toString = function () { return this.message; }; jasmine.ExpectationResult.prototype.passed = function () { return this.passed_; }; /** * Getter for the Jasmine environment. Ensures one gets created */ jasmine.getEnv = function() { var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); return env; }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isArray_ = function(value) { return jasmine.isA_("Array", value); }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isString_ = function(value) { return jasmine.isA_("String", value); }; /** * @ignore * @private * @param value * @returns {Boolean} */ jasmine.isNumber_ = function(value) { return jasmine.isA_("Number", value); }; /** * @ignore * @private * @param {String} typeName * @param value * @returns {Boolean} */ jasmine.isA_ = function(typeName, value) { return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; }; /** * Pretty printer for expecations. Takes any object and turns it into a human-readable string. * * @param value {Object} an object to be outputted * @returns {String} */ jasmine.pp = function(value) { var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); stringPrettyPrinter.format(value); return stringPrettyPrinter.string; }; /** * Returns true if the object is a DOM Node. * * @param {Object} obj object to check * @returns {Boolean} */ jasmine.isDomNode = function(obj) { return obj.nodeType > 0; }; /** * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. * * @example * // don't care about which function is passed in, as long as it's a function * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); * * @param {Class} clazz * @returns matchable object of the type clazz */ jasmine.any = function(clazz) { return new jasmine.Matchers.Any(clazz); }; /** * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. * * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine * expectation syntax. Spies can be checked if they were called or not and what the calling params were. * * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). * * Spies are torn down at the end of every spec. * * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. * * @example * // a stub * var myStub = jasmine.createSpy('myStub'); // can be used anywhere * * // spy example * var foo = { * not: function(bool) { return !bool; } * } * * // actual foo.not will not be called, execution stops * spyOn(foo, 'not'); // foo.not spied upon, execution will continue to implementation * spyOn(foo, 'not').andCallThrough(); * * // fake example * var foo = { * not: function(bool) { return !bool; } * } * * // foo.not(val) will return val * spyOn(foo, 'not').andCallFake(function(value) {return value;}); * * // mock example * foo.not(7 == 7); * expect(foo.not).toHaveBeenCalled(); * expect(foo.not).toHaveBeenCalledWith(true); * * @constructor * @see spyOn, jasmine.createSpy, jasmine.createSpyObj * @param {String} name */ jasmine.Spy = function(name) { /** * The name of the spy, if provided. */ this.identity = name || 'unknown'; /** * Is this Object a spy? */ this.isSpy = true; /** * The actual function this spy stubs. */ this.plan = function() { }; /** * Tracking of the most recent call to the spy. * @example * var mySpy = jasmine.createSpy('foo'); * mySpy(1, 2); * mySpy.mostRecentCall.args = [1, 2]; */ this.mostRecentCall = {}; /** * Holds arguments for each call to the spy, indexed by call count * @example * var mySpy = jasmine.createSpy('foo'); * mySpy(1, 2); * mySpy(7, 8); * mySpy.mostRecentCall.args = [7, 8]; * mySpy.argsForCall[0] = [1, 2]; * mySpy.argsForCall[1] = [7, 8]; */ this.argsForCall = []; this.calls = []; }; /** * Tells a spy to call through to the actual implemenatation. * * @example * var foo = { * bar: function() { // do some stuff } * } * * // defining a spy on an existing property: foo.bar * spyOn(foo, 'bar').andCallThrough(); */ jasmine.Spy.prototype.andCallThrough = function() { this.plan = this.originalValue; return this; }; /** * For setting the return value of a spy. * * @example * // defining a spy from scratch: foo() returns 'baz' * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); * * // defining a spy on an existing property: foo.bar() returns 'baz' * spyOn(foo, 'bar').andReturn('baz'); * * @param {Object} value */ jasmine.Spy.prototype.andReturn = function(value) { this.plan = function() { return value; }; return this; }; /** * For throwing an exception when a spy is called. * * @example * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); * * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' * spyOn(foo, 'bar').andThrow('baz'); * * @param {String} exceptionMsg */ jasmine.Spy.prototype.andThrow = function(exceptionMsg) { this.plan = function() { throw exceptionMsg; }; return this; }; /** * Calls an alternate implementation when a spy is called. * * @example * var baz = function() { * // do some stuff, return something * } * // defining a spy from scratch: foo() calls the function baz * var foo = jasmine.createSpy('spy on foo').andCall(baz); * * // defining a spy on an existing property: foo.bar() calls an anonymnous function * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); * * @param {Function} fakeFunc */ jasmine.Spy.prototype.andCallFake = function(fakeFunc) { this.plan = fakeFunc; return this; }; /** * Resets all of a spy's the tracking variables so that it can be used again. * * @example * spyOn(foo, 'bar'); * * foo.bar(); * * expect(foo.bar.callCount).toEqual(1); * * foo.bar.reset(); * * expect(foo.bar.callCount).toEqual(0); */ jasmine.Spy.prototype.reset = function() { this.wasCalled = false; this.callCount = 0; this.argsForCall = []; this.calls = []; this.mostRecentCall = {}; }; jasmine.createSpy = function(name) { var spyObj = function() { spyObj.wasCalled = true; spyObj.callCount++; var args = jasmine.util.argsToArray(arguments); spyObj.mostRecentCall.object = this; spyObj.mostRecentCall.args = args; spyObj.argsForCall.push(args); spyObj.calls.push({object: this, args: args}); return spyObj.plan.apply(this, arguments); }; var spy = new jasmine.Spy(name); for (var prop in spy) { spyObj[prop] = spy[prop]; } spyObj.reset(); return spyObj; }; /** * Determines whether an object is a spy. * * @param {jasmine.Spy|Object} putativeSpy * @returns {Boolean} */ jasmine.isSpy = function(putativeSpy) { return putativeSpy && putativeSpy.isSpy; }; /** * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something * large in one call. * * @param {String} baseName name of spy class * @param {Array} methodNames array of names of methods to make spies */ jasmine.createSpyObj = function(baseName, methodNames) { if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); } var obj = {}; for (var i = 0; i < methodNames.length; i++) { obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); } return obj; }; /** * All parameters are pretty-printed and concatenated together, then written to the current spec's output. * * Be careful not to leave calls to jasmine.log in production code. */ jasmine.log = function() { var spec = jasmine.getEnv().currentSpec; spec.log.apply(spec, arguments); }; /** * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. * * @example * // spy example * var foo = { * not: function(bool) { return !bool; } * } * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops * * @see jasmine.createSpy * @param obj * @param methodName * @returns a Jasmine spy that can be chained with all spy methods */ var spyOn = function(obj, methodName) { return jasmine.getEnv().currentSpec.spyOn(obj, methodName); }; if (isCommonJS) exports.spyOn = spyOn; /** * Creates a Jasmine spec that will be added to the current suite. * * // TODO: pending tests * * @example * it('should be true', function() { * expect(true).toEqual(true); * }); * * @param {String} desc description of this specification * @param {Function} func defines the preconditions and expectations of the spec */ var it = function(desc, func) { return jasmine.getEnv().it(desc, func); }; if (isCommonJS) exports.it = it; /** * Creates a disabled Jasmine spec. * * A convenience method that allows existing specs to be disabled temporarily during development. * * @param {String} desc description of this specification * @param {Function} func defines the preconditions and expectations of the spec */ var xit = function(desc, func) { return jasmine.getEnv().xit(desc, func); }; if (isCommonJS) exports.xit = xit; /** * Starts a chain for a Jasmine expectation. * * It is passed an Object that is the actual value and should chain to one of the many * jasmine.Matchers functions. * * @param {Object} actual Actual value to test against and expected value */ var expect = function(actual) { return jasmine.getEnv().currentSpec.expect(actual); }; if (isCommonJS) exports.expect = expect; /** * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. * * @param {Function} func Function that defines part of a jasmine spec. */ var runs = function(func) { jasmine.getEnv().currentSpec.runs(func); }; if (isCommonJS) exports.runs = runs; /** * Waits a fixed time period before moving to the next block. * * @deprecated Use waitsFor() instead * @param {Number} timeout milliseconds to wait */ var waits = function(timeout) { jasmine.getEnv().currentSpec.waits(timeout); }; if (isCommonJS) exports.waits = waits; /** * Waits for the latchFunction to return true before proceeding to the next block. * * @param {Function} latchFunction * @param {String} optional_timeoutMessage * @param {Number} optional_timeout */ var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); }; if (isCommonJS) exports.waitsFor = waitsFor; /** * A function that is called before each spec in a suite. * * Used for spec setup, including validating assumptions. * * @param {Function} beforeEachFunction */ var beforeEach = function(beforeEachFunction) { jasmine.getEnv().beforeEach(beforeEachFunction); }; if (isCommonJS) exports.beforeEach = beforeEach; /** * A function that is called after each spec in a suite. * * Used for restoring any state that is hijacked during spec execution. * * @param {Function} afterEachFunction */ var afterEach = function(afterEachFunction) { jasmine.getEnv().afterEach(afterEachFunction); }; if (isCommonJS) exports.afterEach = afterEach; /** * Defines a suite of specifications. * * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization * of setup in some tests. * * @example * // TODO: a simple suite * * // TODO: a simple suite with a nested describe block * * @param {String} description A string, usually the class under test. * @param {Function} specDefinitions function that defines several specs. */ var describe = function(description, specDefinitions) { return jasmine.getEnv().describe(description, specDefinitions); }; if (isCommonJS) exports.describe = describe; /** * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. * * @param {String} description A string, usually the class under test. * @param {Function} specDefinitions function that defines several specs. */ var xdescribe = function(description, specDefinitions) { return jasmine.getEnv().xdescribe(description, specDefinitions); }; if (isCommonJS) exports.xdescribe = xdescribe; // Provide the XMLHttpRequest class for IE 5.x-6.x: jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { function tryIt(f) { try { return f(); } catch(e) { } return null; } var xhr = tryIt(function() { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); }) || tryIt(function() { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); }) || tryIt(function() { return new ActiveXObject("Msxml2.XMLHTTP"); }) || tryIt(function() { return new ActiveXObject("Microsoft.XMLHTTP"); }); if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); return xhr; } : XMLHttpRequest; /** * @namespace */ jasmine.util = {}; /** * Declare that a child class inherit it's prototype from the parent class. * * @private * @param {Function} childClass * @param {Function} parentClass */ jasmine.util.inherit = function(childClass, parentClass) { /** * @private */ var subclass = function() { }; subclass.prototype = parentClass.prototype; childClass.prototype = new subclass(); }; jasmine.util.formatException = function(e) { var lineNumber; if (e.line) { lineNumber = e.line; } else if (e.lineNumber) { lineNumber = e.lineNumber; } var file; if (e.sourceURL) { file = e.sourceURL; } else if (e.fileName) { file = e.fileName; } var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); if (file && lineNumber) { message += ' in ' + file + ' (line ' + lineNumber + ')'; } return message; }; jasmine.util.htmlEscape = function(str) { if (!str) return str; return str.replace(/&/g, '&') .replace(//g, '>'); }; jasmine.util.argsToArray = function(args) { var arrayOfArgs = []; for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); return arrayOfArgs; }; jasmine.util.extend = function(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }; /** * Environment for Jasmine * * @constructor */ jasmine.Env = function() { this.currentSpec = null; this.currentSuite = null; this.currentRunner_ = new jasmine.Runner(this); this.reporter = new jasmine.MultiReporter(); this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; this.lastUpdate = 0; this.specFilter = function() { return true; }; this.nextSpecId_ = 0; this.nextSuiteId_ = 0; this.equalityTesters_ = []; // wrap matchers this.matchersClass = function() { jasmine.Matchers.apply(this, arguments); }; jasmine.util.inherit(this.matchersClass, jasmine.Matchers); jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); }; jasmine.Env.prototype.setTimeout = jasmine.setTimeout; jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; jasmine.Env.prototype.setInterval = jasmine.setInterval; jasmine.Env.prototype.clearInterval = jasmine.clearInterval; /** * @returns an object containing jasmine version build info, if set. */ jasmine.Env.prototype.version = function () { if (jasmine.version_) { return jasmine.version_; } else { throw new Error('Version not set'); } }; /** * @returns string containing jasmine version build info, if set. */ jasmine.Env.prototype.versionString = function() { if (!jasmine.version_) { return "version unknown"; } var version = this.version(); var dotted_version = version.major + "." + version.minor + "." + version.build; if (version.rc) { dotted_version += ".rc" + version.rc; } return dotted_version + " revision " + version.revision; }; /** * @returns a sequential integer starting at 0 */ jasmine.Env.prototype.nextSpecId = function () { return this.nextSpecId_++; }; /** * @returns a sequential integer starting at 0 */ jasmine.Env.prototype.nextSuiteId = function () { return this.nextSuiteId_++; }; /** * Register a reporter to receive status updates from Jasmine. * @param {jasmine.Reporter} reporter An object which will receive status updates. */ jasmine.Env.prototype.addReporter = function(reporter) { this.reporter.addReporter(reporter); }; jasmine.Env.prototype.execute = function() { this.currentRunner_.execute(); }; jasmine.Env.prototype.describe = function(description, specDefinitions) { var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); var parentSuite = this.currentSuite; if (parentSuite) { parentSuite.add(suite); } else { this.currentRunner_.add(suite); } this.currentSuite = suite; var declarationError = null; try { specDefinitions.call(suite); } catch(e) { declarationError = e; } if (declarationError) { this.it("encountered a declaration exception", function() { throw declarationError; }); } this.currentSuite = parentSuite; return suite; }; jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { if (this.currentSuite) { this.currentSuite.beforeEach(beforeEachFunction); } else { this.currentRunner_.beforeEach(beforeEachFunction); } }; jasmine.Env.prototype.currentRunner = function () { return this.currentRunner_; }; jasmine.Env.prototype.afterEach = function(afterEachFunction) { if (this.currentSuite) { this.currentSuite.afterEach(afterEachFunction); } else { this.currentRunner_.afterEach(afterEachFunction); } }; jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { return { execute: function() { } }; }; jasmine.Env.prototype.it = function(description, func) { var spec = new jasmine.Spec(this, this.currentSuite, description); this.currentSuite.add(spec); this.currentSpec = spec; if (func) { spec.runs(func); } return spec; }; jasmine.Env.prototype.xit = function(desc, func) { return { id: this.nextSpecId(), runs: function() { } }; }; jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { return true; } a.__Jasmine_been_here_before__ = b; b.__Jasmine_been_here_before__ = a; var hasKey = function(obj, keyName) { return obj !== null && obj[keyName] !== jasmine.undefined; }; for (var property in b) { if (!hasKey(a, property) && hasKey(b, property)) { mismatchKeys.push("expected has key '" + property + "', but missing from actual."); } } for (property in a) { if (!hasKey(b, property) && hasKey(a, property)) { mismatchKeys.push("expected missing key '" + property + "', but present in actual."); } } for (property in b) { if (property == '__Jasmine_been_here_before__') continue; if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); } } if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { mismatchValues.push("arrays were not the same length"); } delete a.__Jasmine_been_here_before__; delete b.__Jasmine_been_here_before__; return (mismatchKeys.length === 0 && mismatchValues.length === 0); }; jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { mismatchKeys = mismatchKeys || []; mismatchValues = mismatchValues || []; for (var i = 0; i < this.equalityTesters_.length; i++) { var equalityTester = this.equalityTesters_[i]; var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); if (result !== jasmine.undefined) return result; } if (a === b) return true; if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { return (a == jasmine.undefined && b == jasmine.undefined); } if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { return a === b; } if (a instanceof Date && b instanceof Date) { return a.getTime() == b.getTime(); } if (a instanceof jasmine.Matchers.Any) { return a.matches(b); } if (b instanceof jasmine.Matchers.Any) { return b.matches(a); } if (jasmine.isString_(a) && jasmine.isString_(b)) { return (a == b); } if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { return (a == b); } if (typeof a === "object" && typeof b === "object") { return this.compareObjects_(a, b, mismatchKeys, mismatchValues); } //Straight check return (a === b); }; jasmine.Env.prototype.contains_ = function(haystack, needle) { if (jasmine.isArray_(haystack)) { for (var i = 0; i < haystack.length; i++) { if (this.equals_(haystack[i], needle)) return true; } return false; } return haystack.indexOf(needle) >= 0; }; jasmine.Env.prototype.addEqualityTester = function(equalityTester) { this.equalityTesters_.push(equalityTester); }; /** No-op base class for Jasmine reporters. * * @constructor */ jasmine.Reporter = function() { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportRunnerResults = function(runner) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSuiteResults = function(suite) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSpecStarting = function(spec) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.reportSpecResults = function(spec) { }; //noinspection JSUnusedLocalSymbols jasmine.Reporter.prototype.log = function(str) { }; /** * Blocks are functions with executable code that make up a spec. * * @constructor * @param {jasmine.Env} env * @param {Function} func * @param {jasmine.Spec} spec */ jasmine.Block = function(env, func, spec) { this.env = env; this.func = func; this.spec = spec; }; jasmine.Block.prototype.execute = function(onComplete) { try { this.func.apply(this.spec); } catch (e) { this.spec.fail(e); } onComplete(); }; /** JavaScript API reporter. * * @constructor */ jasmine.JsApiReporter = function() { this.started = false; this.finished = false; this.suites_ = []; this.results_ = {}; }; jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { this.started = true; var suites = runner.topLevelSuites(); for (var i = 0; i < suites.length; i++) { var suite = suites[i]; this.suites_.push(this.summarize_(suite)); } }; jasmine.JsApiReporter.prototype.suites = function() { return this.suites_; }; jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { var isSuite = suiteOrSpec instanceof jasmine.Suite; var summary = { id: suiteOrSpec.id, name: suiteOrSpec.description, type: isSuite ? 'suite' : 'spec', children: [] }; if (isSuite) { var children = suiteOrSpec.children(); for (var i = 0; i < children.length; i++) { summary.children.push(this.summarize_(children[i])); } } return summary; }; jasmine.JsApiReporter.prototype.results = function() { return this.results_; }; jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { return this.results_[specId]; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { this.finished = true; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { this.results_[spec.id] = { messages: spec.results().getItems(), result: spec.results().failedCount > 0 ? "failed" : "passed" }; }; //noinspection JSUnusedLocalSymbols jasmine.JsApiReporter.prototype.log = function(str) { }; jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ var results = {}; for (var i = 0; i < specIds.length; i++) { var specId = specIds[i]; results[specId] = this.summarizeResult_(this.results_[specId]); } return results; }; jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ var summaryMessages = []; var messagesLength = result.messages.length; for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { var resultMessage = result.messages[messageIndex]; summaryMessages.push({ text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, passed: resultMessage.passed ? resultMessage.passed() : true, type: resultMessage.type, message: resultMessage.message, trace: { stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined } }); } return { result : result.result, messages : summaryMessages }; }; /** * @constructor * @param {jasmine.Env} env * @param actual * @param {jasmine.Spec} spec */ jasmine.Matchers = function(env, actual, spec, opt_isNot) { this.env = env; this.actual = actual; this.spec = spec; this.isNot = opt_isNot || false; this.reportWasCalled_ = false; }; // todo: @deprecated as of Jasmine 0.11, remove soon [xw] jasmine.Matchers.pp = function(str) { throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); }; // todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] jasmine.Matchers.prototype.report = function(result, failing_message, details) { throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); }; jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { for (var methodName in prototype) { if (methodName == 'report') continue; var orig = prototype[methodName]; matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); } }; jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { return function() { var matcherArgs = jasmine.util.argsToArray(arguments); var result = matcherFunction.apply(this, arguments); if (this.isNot) { result = !result; } if (this.reportWasCalled_) return result; var message; if (!result) { if (this.message) { message = this.message.apply(this, arguments); if (jasmine.isArray_(message)) { message = message[this.isNot ? 1 : 0]; } } else { var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; if (matcherArgs.length > 0) { for (var i = 0; i < matcherArgs.length; i++) { if (i > 0) message += ","; message += " " + jasmine.pp(matcherArgs[i]); } } message += "."; } } var expectationResult = new jasmine.ExpectationResult({ matcherName: matcherName, passed: result, expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], actual: this.actual, message: message }); this.spec.addMatcherResult(expectationResult); return jasmine.undefined; }; }; /** * toBe: compares the actual to the expected using === * @param expected */ jasmine.Matchers.prototype.toBe = function(expected) { return this.actual === expected; }; /** * toNotBe: compares the actual to the expected using !== * @param expected * @deprecated as of 1.0. Use not.toBe() instead. */ jasmine.Matchers.prototype.toNotBe = function(expected) { return this.actual !== expected; }; /** * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. * * @param expected */ jasmine.Matchers.prototype.toEqual = function(expected) { return this.env.equals_(this.actual, expected); }; /** * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual * @param expected * @deprecated as of 1.0. Use not.toNotEqual() instead. */ jasmine.Matchers.prototype.toNotEqual = function(expected) { return !this.env.equals_(this.actual, expected); }; /** * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes * a pattern or a String. * * @param expected */ jasmine.Matchers.prototype.toMatch = function(expected) { return new RegExp(expected).test(this.actual); }; /** * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch * @param expected * @deprecated as of 1.0. Use not.toMatch() instead. */ jasmine.Matchers.prototype.toNotMatch = function(expected) { return !(new RegExp(expected).test(this.actual)); }; /** * Matcher that compares the actual to jasmine.undefined. */ jasmine.Matchers.prototype.toBeDefined = function() { return (this.actual !== jasmine.undefined); }; /** * Matcher that compares the actual to jasmine.undefined. */ jasmine.Matchers.prototype.toBeUndefined = function() { return (this.actual === jasmine.undefined); }; /** * Matcher that compares the actual to null. */ jasmine.Matchers.prototype.toBeNull = function() { return (this.actual === null); }; /** * Matcher that boolean not-nots the actual. */ jasmine.Matchers.prototype.toBeTruthy = function() { return !!this.actual; }; /** * Matcher that boolean nots the actual. */ jasmine.Matchers.prototype.toBeFalsy = function() { return !this.actual; }; /** * Matcher that checks to see if the actual, a Jasmine spy, was called. */ jasmine.Matchers.prototype.toHaveBeenCalled = function() { if (arguments.length > 0) { throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); } if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy " + this.actual.identity + " to have been called.", "Expected spy " + this.actual.identity + " not to have been called." ]; }; return this.actual.wasCalled; }; /** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; /** * Matcher that checks to see if the actual, a Jasmine spy, was not called. * * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead */ jasmine.Matchers.prototype.wasNotCalled = function() { if (arguments.length > 0) { throw new Error('wasNotCalled does not take arguments'); } if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy " + this.actual.identity + " to not have been called.", "Expected spy " + this.actual.identity + " to have been called." ]; }; return !this.actual.wasCalled; }; /** * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. * * @example * */ jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { var expectedArgs = jasmine.util.argsToArray(arguments); if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { if (this.actual.callCount === 0) { // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] return [ "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." ]; } else { return [ "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) ]; } }; return this.env.contains_(this.actual.argsForCall, expectedArgs); }; /** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; /** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ jasmine.Matchers.prototype.wasNotCalledWith = function() { var expectedArgs = jasmine.util.argsToArray(arguments); if (!jasmine.isSpy(this.actual)) { throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); } this.message = function() { return [ "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" ]; }; return !this.env.contains_(this.actual.argsForCall, expectedArgs); }; /** * Matcher that checks that the expected item is an element in the actual Array. * * @param {Object} expected */ jasmine.Matchers.prototype.toContain = function(expected) { return this.env.contains_(this.actual, expected); }; /** * Matcher that checks that the expected item is NOT an element in the actual Array. * * @param {Object} expected * @deprecated as of 1.0. Use not.toNotContain() instead. */ jasmine.Matchers.prototype.toNotContain = function(expected) { return !this.env.contains_(this.actual, expected); }; jasmine.Matchers.prototype.toBeLessThan = function(expected) { return this.actual < expected; }; jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { return this.actual > expected; }; /** * Matcher that checks that the expected item is equal to the actual item * up to a given level of decimal precision (default 2). * * @param {Number} expected * @param {Number} precision */ jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { if (!(precision === 0)) { precision = precision || 2; } var multiplier = Math.pow(10, precision); var actual = Math.round(this.actual * multiplier); expected = Math.round(expected * multiplier); return expected == actual; }; /** * Matcher that checks that the expected exception was thrown by the actual. * * @param {String} expected */ jasmine.Matchers.prototype.toThrow = function(expected) { var result = false; var exception; if (typeof this.actual != 'function') { throw new Error('Actual is not a function'); } try { this.actual(); } catch (e) { exception = e; } if (exception) { result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); } var not = this.isNot ? "not " : ""; this.message = function() { if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); } else { return "Expected function to throw an exception."; } }; return result; }; jasmine.Matchers.Any = function(expectedClass) { this.expectedClass = expectedClass; }; jasmine.Matchers.Any.prototype.matches = function(other) { if (this.expectedClass == String) { return typeof other == 'string' || other instanceof String; } if (this.expectedClass == Number) { return typeof other == 'number' || other instanceof Number; } if (this.expectedClass == Function) { return typeof other == 'function' || other instanceof Function; } if (this.expectedClass == Object) { return typeof other == 'object'; } return other instanceof this.expectedClass; }; jasmine.Matchers.Any.prototype.toString = function() { return ''; }; /** * @constructor */ jasmine.MultiReporter = function() { this.subReporters_ = []; }; jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); jasmine.MultiReporter.prototype.addReporter = function(reporter) { this.subReporters_.push(reporter); }; (function() { var functionNames = [ "reportRunnerStarting", "reportRunnerResults", "reportSuiteResults", "reportSpecStarting", "reportSpecResults", "log" ]; for (var i = 0; i < functionNames.length; i++) { var functionName = functionNames[i]; jasmine.MultiReporter.prototype[functionName] = (function(functionName) { return function() { for (var j = 0; j < this.subReporters_.length; j++) { var subReporter = this.subReporters_[j]; if (subReporter[functionName]) { subReporter[functionName].apply(subReporter, arguments); } } }; })(functionName); } })(); /** * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults * * @constructor */ jasmine.NestedResults = function() { /** * The total count of results */ this.totalCount = 0; /** * Number of passed results */ this.passedCount = 0; /** * Number of failed results */ this.failedCount = 0; /** * Was this suite/spec skipped? */ this.skipped = false; /** * @ignore */ this.items_ = []; }; /** * Roll up the result counts. * * @param result */ jasmine.NestedResults.prototype.rollupCounts = function(result) { this.totalCount += result.totalCount; this.passedCount += result.passedCount; this.failedCount += result.failedCount; }; /** * Adds a log message. * @param values Array of message parts which will be concatenated later. */ jasmine.NestedResults.prototype.log = function(values) { this.items_.push(new jasmine.MessageResult(values)); }; /** * Getter for the results: message & results. */ jasmine.NestedResults.prototype.getItems = function() { return this.items_; }; /** * Adds a result, tracking counts (total, passed, & failed) * @param {jasmine.ExpectationResult|jasmine.NestedResults} result */ jasmine.NestedResults.prototype.addResult = function(result) { if (result.type != 'log') { if (result.items_) { this.rollupCounts(result); } else { this.totalCount++; if (result.passed()) { this.passedCount++; } else { this.failedCount++; } } } this.items_.push(result); }; /** * @returns {Boolean} True if everything below passed */ jasmine.NestedResults.prototype.passed = function() { return this.passedCount === this.totalCount; }; /** * Base class for pretty printing for expectation results. */ jasmine.PrettyPrinter = function() { this.ppNestLevel_ = 0; }; /** * Formats a value in a nice, human-readable string. * * @param value */ jasmine.PrettyPrinter.prototype.format = function(value) { if (this.ppNestLevel_ > 40) { throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); } this.ppNestLevel_++; try { if (value === jasmine.undefined) { this.emitScalar('undefined'); } else if (value === null) { this.emitScalar('null'); } else if (value === jasmine.getGlobal()) { this.emitScalar(''); } else if (value instanceof jasmine.Matchers.Any) { this.emitScalar(value.toString()); } else if (typeof value === 'string') { this.emitString(value); } else if (jasmine.isSpy(value)) { this.emitScalar("spy on " + value.identity); } else if (value instanceof RegExp) { this.emitScalar(value.toString()); } else if (typeof value === 'function') { this.emitScalar('Function'); } else if (typeof value.nodeType === 'number') { this.emitScalar('HTMLNode'); } else if (value instanceof Date) { this.emitScalar('Date(' + value + ')'); } else if (value.__Jasmine_been_here_before__) { this.emitScalar(''); } else if (jasmine.isArray_(value) || typeof value == 'object') { value.__Jasmine_been_here_before__ = true; if (jasmine.isArray_(value)) { this.emitArray(value); } else { this.emitObject(value); } delete value.__Jasmine_been_here_before__; } else { this.emitScalar(value.toString()); } } finally { this.ppNestLevel_--; } }; jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { for (var property in obj) { if (property == '__Jasmine_been_here_before__') continue; fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && obj.__lookupGetter__(property) !== null) : false); } }; jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; jasmine.StringPrettyPrinter = function() { jasmine.PrettyPrinter.call(this); this.string = ''; }; jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { this.append(value); }; jasmine.StringPrettyPrinter.prototype.emitString = function(value) { this.append("'" + value + "'"); }; jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { this.append('[ '); for (var i = 0; i < array.length; i++) { if (i > 0) { this.append(', '); } this.format(array[i]); } this.append(' ]'); }; jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { var self = this; this.append('{ '); var first = true; this.iterateObject(obj, function(property, isGetter) { if (first) { first = false; } else { self.append(', '); } self.append(property); self.append(' : '); if (isGetter) { self.append(''); } else { self.format(obj[property]); } }); this.append(' }'); }; jasmine.StringPrettyPrinter.prototype.append = function(value) { this.string += value; }; jasmine.Queue = function(env) { this.env = env; this.blocks = []; this.running = false; this.index = 0; this.offset = 0; this.abort = false; }; jasmine.Queue.prototype.addBefore = function(block) { this.blocks.unshift(block); }; jasmine.Queue.prototype.add = function(block) { this.blocks.push(block); }; jasmine.Queue.prototype.insertNext = function(block) { this.blocks.splice((this.index + this.offset + 1), 0, block); this.offset++; }; jasmine.Queue.prototype.start = function(onComplete) { this.running = true; this.onComplete = onComplete; this.next_(); }; jasmine.Queue.prototype.isRunning = function() { return this.running; }; jasmine.Queue.LOOP_DONT_RECURSE = true; jasmine.Queue.prototype.next_ = function() { var self = this; var goAgain = true; while (goAgain) { goAgain = false; if (self.index < self.blocks.length && !this.abort) { var calledSynchronously = true; var completedSynchronously = false; var onComplete = function () { if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { completedSynchronously = true; return; } if (self.blocks[self.index].abort) { self.abort = true; } self.offset = 0; self.index++; var now = new Date().getTime(); if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { self.env.lastUpdate = now; self.env.setTimeout(function() { self.next_(); }, 0); } else { if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { goAgain = true; } else { self.next_(); } } }; self.blocks[self.index].execute(onComplete); calledSynchronously = false; if (completedSynchronously) { onComplete(); } } else { self.running = false; if (self.onComplete) { self.onComplete(); } } } }; jasmine.Queue.prototype.results = function() { var results = new jasmine.NestedResults(); for (var i = 0; i < this.blocks.length; i++) { if (this.blocks[i].results) { results.addResult(this.blocks[i].results()); } } return results; }; /** * Runner * * @constructor * @param {jasmine.Env} env */ jasmine.Runner = function(env) { var self = this; self.env = env; self.queue = new jasmine.Queue(env); self.before_ = []; self.after_ = []; self.suites_ = []; }; jasmine.Runner.prototype.execute = function() { var self = this; if (self.env.reporter.reportRunnerStarting) { self.env.reporter.reportRunnerStarting(this); } self.queue.start(function () { self.finishCallback(); }); }; jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { beforeEachFunction.typeName = 'beforeEach'; this.before_.splice(0,0,beforeEachFunction); }; jasmine.Runner.prototype.afterEach = function(afterEachFunction) { afterEachFunction.typeName = 'afterEach'; this.after_.splice(0,0,afterEachFunction); }; jasmine.Runner.prototype.finishCallback = function() { this.env.reporter.reportRunnerResults(this); }; jasmine.Runner.prototype.addSuite = function(suite) { this.suites_.push(suite); }; jasmine.Runner.prototype.add = function(block) { if (block instanceof jasmine.Suite) { this.addSuite(block); } this.queue.add(block); }; jasmine.Runner.prototype.specs = function () { var suites = this.suites(); var specs = []; for (var i = 0; i < suites.length; i++) { specs = specs.concat(suites[i].specs()); } return specs; }; jasmine.Runner.prototype.suites = function() { return this.suites_; }; jasmine.Runner.prototype.topLevelSuites = function() { var topLevelSuites = []; for (var i = 0; i < this.suites_.length; i++) { if (!this.suites_[i].parentSuite) { topLevelSuites.push(this.suites_[i]); } } return topLevelSuites; }; jasmine.Runner.prototype.results = function() { return this.queue.results(); }; /** * Internal representation of a Jasmine specification, or test. * * @constructor * @param {jasmine.Env} env * @param {jasmine.Suite} suite * @param {String} description */ jasmine.Spec = function(env, suite, description) { if (!env) { throw new Error('jasmine.Env() required'); } if (!suite) { throw new Error('jasmine.Suite() required'); } var spec = this; spec.id = env.nextSpecId ? env.nextSpecId() : null; spec.env = env; spec.suite = suite; spec.description = description; spec.queue = new jasmine.Queue(env); spec.afterCallbacks = []; spec.spies_ = []; spec.results_ = new jasmine.NestedResults(); spec.results_.description = description; spec.matchersClass = null; }; jasmine.Spec.prototype.getFullName = function() { return this.suite.getFullName() + ' ' + this.description + '.'; }; jasmine.Spec.prototype.results = function() { return this.results_; }; /** * All parameters are pretty-printed and concatenated together, then written to the spec's output. * * Be careful not to leave calls to jasmine.log in production code. */ jasmine.Spec.prototype.log = function() { return this.results_.log(arguments); }; jasmine.Spec.prototype.runs = function (func) { var block = new jasmine.Block(this.env, func, this); this.addToQueue(block); return this; }; jasmine.Spec.prototype.addToQueue = function (block) { if (this.queue.isRunning()) { this.queue.insertNext(block); } else { this.queue.add(block); } }; /** * @param {jasmine.ExpectationResult} result */ jasmine.Spec.prototype.addMatcherResult = function(result) { this.results_.addResult(result); }; jasmine.Spec.prototype.expect = function(actual) { var positive = new (this.getMatchersClass_())(this.env, actual, this); positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); return positive; }; /** * Waits a fixed time period before moving to the next block. * * @deprecated Use waitsFor() instead * @param {Number} timeout milliseconds to wait */ jasmine.Spec.prototype.waits = function(timeout) { var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); this.addToQueue(waitsFunc); return this; }; /** * Waits for the latchFunction to return true before proceeding to the next block. * * @param {Function} latchFunction * @param {String} optional_timeoutMessage * @param {Number} optional_timeout */ jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { var latchFunction_ = null; var optional_timeoutMessage_ = null; var optional_timeout_ = null; for (var i = 0; i < arguments.length; i++) { var arg = arguments[i]; switch (typeof arg) { case 'function': latchFunction_ = arg; break; case 'string': optional_timeoutMessage_ = arg; break; case 'number': optional_timeout_ = arg; break; } } var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); this.addToQueue(waitsForFunc); return this; }; jasmine.Spec.prototype.fail = function (e) { var expectationResult = new jasmine.ExpectationResult({ passed: false, message: e ? jasmine.util.formatException(e) : 'Exception', trace: { stack: e.stack } }); this.results_.addResult(expectationResult); }; jasmine.Spec.prototype.getMatchersClass_ = function() { return this.matchersClass || this.env.matchersClass; }; jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { var parent = this.getMatchersClass_(); var newMatchersClass = function() { parent.apply(this, arguments); }; jasmine.util.inherit(newMatchersClass, parent); jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); this.matchersClass = newMatchersClass; }; jasmine.Spec.prototype.finishCallback = function() { this.env.reporter.reportSpecResults(this); }; jasmine.Spec.prototype.finish = function(onComplete) { this.removeAllSpies(); this.finishCallback(); if (onComplete) { onComplete(); } }; jasmine.Spec.prototype.after = function(doAfter) { if (this.queue.isRunning()) { this.queue.add(new jasmine.Block(this.env, doAfter, this)); } else { this.afterCallbacks.unshift(doAfter); } }; jasmine.Spec.prototype.execute = function(onComplete) { var spec = this; if (!spec.env.specFilter(spec)) { spec.results_.skipped = true; spec.finish(onComplete); return; } this.env.reporter.reportSpecStarting(this); spec.env.currentSpec = spec; spec.addBeforesAndAftersToQueue(); spec.queue.start(function () { spec.finish(onComplete); }); }; jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { var runner = this.env.currentRunner(); var i; for (var suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.before_.length; i++) { this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); } } for (i = 0; i < runner.before_.length; i++) { this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); } for (i = 0; i < this.afterCallbacks.length; i++) { this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); } for (suite = this.suite; suite; suite = suite.parentSuite) { for (i = 0; i < suite.after_.length; i++) { this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); } } for (i = 0; i < runner.after_.length; i++) { this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); } }; jasmine.Spec.prototype.explodes = function() { throw 'explodes function should not have been called'; }; jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { if (obj == jasmine.undefined) { throw "spyOn could not find an object to spy upon for " + methodName + "()"; } if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { throw methodName + '() method does not exist'; } if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { throw new Error(methodName + ' has already been spied upon'); } var spyObj = jasmine.createSpy(methodName); this.spies_.push(spyObj); spyObj.baseObj = obj; spyObj.methodName = methodName; spyObj.originalValue = obj[methodName]; obj[methodName] = spyObj; return spyObj; }; jasmine.Spec.prototype.removeAllSpies = function() { for (var i = 0; i < this.spies_.length; i++) { var spy = this.spies_[i]; spy.baseObj[spy.methodName] = spy.originalValue; } this.spies_ = []; }; /** * Internal representation of a Jasmine suite. * * @constructor * @param {jasmine.Env} env * @param {String} description * @param {Function} specDefinitions * @param {jasmine.Suite} parentSuite */ jasmine.Suite = function(env, description, specDefinitions, parentSuite) { var self = this; self.id = env.nextSuiteId ? env.nextSuiteId() : null; self.description = description; self.queue = new jasmine.Queue(env); self.parentSuite = parentSuite; self.env = env; self.before_ = []; self.after_ = []; self.children_ = []; self.suites_ = []; self.specs_ = []; }; jasmine.Suite.prototype.getFullName = function() { var fullName = this.description; for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { fullName = parentSuite.description + ' ' + fullName; } return fullName; }; jasmine.Suite.prototype.finish = function(onComplete) { this.env.reporter.reportSuiteResults(this); this.finished = true; if (typeof(onComplete) == 'function') { onComplete(); } }; jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { beforeEachFunction.typeName = 'beforeEach'; this.before_.unshift(beforeEachFunction); }; jasmine.Suite.prototype.afterEach = function(afterEachFunction) { afterEachFunction.typeName = 'afterEach'; this.after_.unshift(afterEachFunction); }; jasmine.Suite.prototype.results = function() { return this.queue.results(); }; jasmine.Suite.prototype.add = function(suiteOrSpec) { this.children_.push(suiteOrSpec); if (suiteOrSpec instanceof jasmine.Suite) { this.suites_.push(suiteOrSpec); this.env.currentRunner().addSuite(suiteOrSpec); } else { this.specs_.push(suiteOrSpec); } this.queue.add(suiteOrSpec); }; jasmine.Suite.prototype.specs = function() { return this.specs_; }; jasmine.Suite.prototype.suites = function() { return this.suites_; }; jasmine.Suite.prototype.children = function() { return this.children_; }; jasmine.Suite.prototype.execute = function(onComplete) { var self = this; this.queue.start(function () { self.finish(onComplete); }); }; jasmine.WaitsBlock = function(env, timeout, spec) { this.timeout = timeout; jasmine.Block.call(this, env, null, spec); }; jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); jasmine.WaitsBlock.prototype.execute = function (onComplete) { if (jasmine.VERBOSE) { this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); } this.env.setTimeout(function () { onComplete(); }, this.timeout); }; /** * A block which waits for some condition to become true, with timeout. * * @constructor * @extends jasmine.Block * @param {jasmine.Env} env The Jasmine environment. * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. * @param {Function} latchFunction A function which returns true when the desired condition has been met. * @param {String} message The message to display if the desired condition hasn't been met within the given time period. * @param {jasmine.Spec} spec The Jasmine spec. */ jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { this.timeout = timeout || env.defaultTimeoutInterval; this.latchFunction = latchFunction; this.message = message; this.totalTimeSpentWaitingForLatch = 0; jasmine.Block.call(this, env, null, spec); }; jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; jasmine.WaitsForBlock.prototype.execute = function(onComplete) { if (jasmine.VERBOSE) { this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); } var latchFunctionResult; try { latchFunctionResult = this.latchFunction.apply(this.spec); } catch (e) { this.spec.fail(e); onComplete(); return; } if (latchFunctionResult) { onComplete(); } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); this.spec.fail({ name: 'timeout', message: message }); this.abort = true; onComplete(); } else { this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; var self = this; this.env.setTimeout(function() { self.execute(onComplete); }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); } }; // Mock setTimeout, clearTimeout // Contributed by Pivotal Computer Systems, www.pivotalsf.com jasmine.FakeTimer = function() { this.reset(); var self = this; self.setTimeout = function(funcToCall, millis) { self.timeoutsMade++; self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); return self.timeoutsMade; }; self.setInterval = function(funcToCall, millis) { self.timeoutsMade++; self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); return self.timeoutsMade; }; self.clearTimeout = function(timeoutKey) { self.scheduledFunctions[timeoutKey] = jasmine.undefined; }; self.clearInterval = function(timeoutKey) { self.scheduledFunctions[timeoutKey] = jasmine.undefined; }; }; jasmine.FakeTimer.prototype.reset = function() { this.timeoutsMade = 0; this.scheduledFunctions = {}; this.nowMillis = 0; }; jasmine.FakeTimer.prototype.tick = function(millis) { var oldMillis = this.nowMillis; var newMillis = oldMillis + millis; this.runFunctionsWithinRange(oldMillis, newMillis); this.nowMillis = newMillis; }; jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { var scheduledFunc; var funcsToRun = []; for (var timeoutKey in this.scheduledFunctions) { scheduledFunc = this.scheduledFunctions[timeoutKey]; if (scheduledFunc != jasmine.undefined && scheduledFunc.runAtMillis >= oldMillis && scheduledFunc.runAtMillis <= nowMillis) { funcsToRun.push(scheduledFunc); this.scheduledFunctions[timeoutKey] = jasmine.undefined; } } if (funcsToRun.length > 0) { funcsToRun.sort(function(a, b) { return a.runAtMillis - b.runAtMillis; }); for (var i = 0; i < funcsToRun.length; ++i) { try { var funcToRun = funcsToRun[i]; this.nowMillis = funcToRun.runAtMillis; funcToRun.funcToCall(); if (funcToRun.recurring) { this.scheduleFunction(funcToRun.timeoutKey, funcToRun.funcToCall, funcToRun.millis, true); } } catch(e) { } } this.runFunctionsWithinRange(oldMillis, nowMillis); } }; jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { this.scheduledFunctions[timeoutKey] = { runAtMillis: this.nowMillis + millis, funcToCall: funcToCall, recurring: recurring, timeoutKey: timeoutKey, millis: millis }; }; /** * @namespace */ jasmine.Clock = { defaultFakeTimer: new jasmine.FakeTimer(), reset: function() { jasmine.Clock.assertInstalled(); jasmine.Clock.defaultFakeTimer.reset(); }, tick: function(millis) { jasmine.Clock.assertInstalled(); jasmine.Clock.defaultFakeTimer.tick(millis); }, runFunctionsWithinRange: function(oldMillis, nowMillis) { jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); }, scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); }, useMock: function() { if (!jasmine.Clock.isInstalled()) { var spec = jasmine.getEnv().currentSpec; spec.after(jasmine.Clock.uninstallMock); jasmine.Clock.installMock(); } }, installMock: function() { jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; }, uninstallMock: function() { jasmine.Clock.assertInstalled(); jasmine.Clock.installed = jasmine.Clock.real; }, real: { setTimeout: jasmine.getGlobal().setTimeout, clearTimeout: jasmine.getGlobal().clearTimeout, setInterval: jasmine.getGlobal().setInterval, clearInterval: jasmine.getGlobal().clearInterval }, assertInstalled: function() { if (!jasmine.Clock.isInstalled()) { throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); } }, isInstalled: function() { return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; }, installed: null }; jasmine.Clock.installed = jasmine.Clock.real; //else for IE support jasmine.getGlobal().setTimeout = function(funcToCall, millis) { if (jasmine.Clock.installed.setTimeout.apply) { return jasmine.Clock.installed.setTimeout.apply(this, arguments); } else { return jasmine.Clock.installed.setTimeout(funcToCall, millis); } }; jasmine.getGlobal().setInterval = function(funcToCall, millis) { if (jasmine.Clock.installed.setInterval.apply) { return jasmine.Clock.installed.setInterval.apply(this, arguments); } else { return jasmine.Clock.installed.setInterval(funcToCall, millis); } }; jasmine.getGlobal().clearTimeout = function(timeoutKey) { if (jasmine.Clock.installed.clearTimeout.apply) { return jasmine.Clock.installed.clearTimeout.apply(this, arguments); } else { return jasmine.Clock.installed.clearTimeout(timeoutKey); } }; jasmine.getGlobal().clearInterval = function(timeoutKey) { if (jasmine.Clock.installed.clearTimeout.apply) { return jasmine.Clock.installed.clearInterval.apply(this, arguments); } else { return jasmine.Clock.installed.clearInterval(timeoutKey); } }; jasmine.version_= { "major": 1, "minor": 1, "build": 0, "revision": 1308187385, "rc": 1 } stamen-modestmaps-js-52d7710/test/browser/lib/jasmine-1.1.0.rc1/jasmine-html.js0000664000175000017500000001542611653546654025734 0ustar daviddavidjasmine.TrivialReporter = function(doc) { this.document = doc || document; this.suiteDivs = {}; this.logRunningSpecs = false; }; jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { var el = document.createElement(type); for (var i = 2; i < arguments.length; i++) { var child = arguments[i]; if (typeof child === 'string') { el.appendChild(document.createTextNode(child)); } else { if (child) { el.appendChild(child); } } } for (var attr in attrs) { if (attr == "className") { el[attr] = attrs[attr]; } else { el.setAttribute(attr, attrs[attr]); } } return el; }; jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { var showPassed, showSkipped; this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, this.createDom('div', { className: 'banner' }, this.createDom('div', { className: 'logo' }, this.createDom('span', { className: 'title' }, "Jasmine"), this.createDom('span', { className: 'version' }, runner.env.versionString())), this.createDom('div', { className: 'options' }, "Show ", showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") ) ), this.runnerDiv = this.createDom('div', { className: 'runner running' }, this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), this.runnerMessageSpan = this.createDom('span', {}, "Running..."), this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) ); this.document.body.appendChild(this.outerDiv); var suites = runner.suites(); for (var i = 0; i < suites.length; i++) { var suite = suites[i]; var suiteDiv = this.createDom('div', { className: 'suite' }, this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); this.suiteDivs[suite.id] = suiteDiv; var parentDiv = this.outerDiv; if (suite.parentSuite) { parentDiv = this.suiteDivs[suite.parentSuite.id]; } parentDiv.appendChild(suiteDiv); } this.startedAt = new Date(); var self = this; showPassed.onclick = function(evt) { if (showPassed.checked) { self.outerDiv.className += ' show-passed'; } else { self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); } }; showSkipped.onclick = function(evt) { if (showSkipped.checked) { self.outerDiv.className += ' show-skipped'; } else { self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); } }; }; jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { var results = runner.results(); var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; this.runnerDiv.setAttribute("class", className); //do it twice for IE this.runnerDiv.setAttribute("className", className); var specs = runner.specs(); var specCount = 0; for (var i = 0; i < specs.length; i++) { if (this.specFilter(specs[i])) { specCount++; } } var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); }; jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { var results = suite.results(); var status = results.passed() ? 'passed' : 'failed'; if (results.totalCount === 0) { // todo: change this to check results.skipped status = 'skipped'; } this.suiteDivs[suite.id].className += " " + status; }; jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { if (this.logRunningSpecs) { this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); } }; jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { var results = spec.results(); var status = results.passed() ? 'passed' : 'failed'; if (results.skipped) { status = 'skipped'; } var specDiv = this.createDom('div', { className: 'spec ' + status }, this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(spec.getFullName()), title: spec.getFullName() }, spec.description)); var resultItems = results.getItems(); var messagesDiv = this.createDom('div', { className: 'messages' }); for (var i = 0; i < resultItems.length; i++) { var result = resultItems[i]; if (result.type == 'log') { messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); } else if (result.type == 'expect' && result.passed && !result.passed()) { messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); if (result.trace.stack) { messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); } } } if (messagesDiv.childNodes.length > 0) { specDiv.appendChild(messagesDiv); } this.suiteDivs[spec.suite.id].appendChild(specDiv); }; jasmine.TrivialReporter.prototype.log = function() { var console = jasmine.getGlobal().console; if (console && console.log) { if (console.log.apply) { console.log.apply(console, arguments); } else { console.log(arguments); // ie fix: console.log.apply doesn't exist on ie } } }; jasmine.TrivialReporter.prototype.getLocation = function() { return this.document.location; }; jasmine.TrivialReporter.prototype.specFilter = function(spec) { var paramMap = {}; var params = this.getLocation().search.substring(1).split('&'); for (var i = 0; i < params.length; i++) { var p = params[i].split('='); paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); } if (!paramMap.spec) { return true; } return spec.getFullName().indexOf(paramMap.spec) === 0; }; stamen-modestmaps-js-52d7710/test/browser/lib/happen.js0000664000175000017500000000302511653546654022042 0ustar daviddavid!(function(context) { var h = {}; // Make inheritance bearable: clone one level of properties function extend(child, parent) { for (var property in parent) { if (typeof child[property] == 'undefined') { child[property] = parent[property]; } } return child; } h.once = function(x, o) { var evt = document.createEvent('MouseEvents'); // https://developer.mozilla.org/en/DOM/event.initMouseEvent evt.initMouseEvent(o.type, true, // canBubble true, // cancelable window, // 'AbstractView' o.detail || 0, // click count o.screenX || 0, // screenX o.screenY || 0, // screenY o.clientX || 0, // clientX o.clientY || 0, // clientY o.ctrl || 0, // ctrl o.alt || false, // alt o.shift || false, // shift o.meta || false, // meta o.button || false, // mouse button null // relatedTarget ); x.dispatchEvent(evt); }; var shortcuts = ['click', 'mousedown', 'mouseup', 'mousemove'], s, i = 0; while (s = shortcuts[i++]) { h[s] = (function(s) { return function(x, o) { h.once(x, extend(o || {}, { type: s })); }; })(s); } h.dblclick = function(x, o) { h.once(x, extend(o || {}, { type: 'dblclick', detail: 2 })); }; this.happen = h; })(this); stamen-modestmaps-js-52d7710/test/browser/index.html0000664000175000017500000000372111653546654021463 0ustar daviddavid Modest Maps Tests stamen-modestmaps-js-52d7710/examples/0000775000175000017500000000000011661034367016627 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/chaining/0000775000175000017500000000000011653546654020417 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/chaining/index.html0000664000175000017500000000125511653546654022417 0ustar daviddavid Modest Maps JS - Chaining Wrapper stamen-modestmaps-js-52d7710/examples/cabs/0000775000175000017500000000000011653546654017547 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/cabs/cabspotting.html0000664000175000017500000000533611653546654022761 0ustar daviddavid Modest Maps JS

Cabspotting JS

zoom in | zoom out
pan left | pan right | pan down | pan up

stamen-modestmaps-js-52d7710/examples/cabs/cab-follower.js0000664000175000017500000000525011653546654022463 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } com.modestmaps.CabFollower = function(map, location, content) { this.coord = map.provider.locationCoordinate(location); this.dimensions = new com.modestmaps.Point(20, 20); this.offset = new com.modestmaps.Point(this.dimensions.x/2, -this.dimensions.y/2); var follower = this; var callback = function(m, a) { return follower.draw(m); }; map.addCallback('panned', callback); map.addCallback('zoomed', callback); map.addCallback('centered', callback); map.addCallback('extentset', callback); this.div = document.createElement('div'); this.div.style.position = 'absolute'; this.div.style.width = this.dimensions.x + 'px'; this.div.style.height = this.dimensions.y + 'px'; var circle = document.createElement('canvas'); this.div.appendChild(circle); if (typeof G_vmlCanvasManager !== 'undefined') circle = G_vmlCanvasManager.initElement(circle); circle.style.position = 'absolute'; circle.style.left = '0px'; circle.style.top = '0px'; circle.width = this.dimensions.x; circle.height = this.dimensions.y; var ctx = circle.getContext("2d"); ctx.lineWidth = 3; ctx.strokeStyle = "rgba(255,255,0,1)"; ctx.fillStyle = "rgba(0,0,0,1)"; ctx.beginPath(); ctx.arc(this.dimensions.x/2, this.dimensions.x/2, -2+this.dimensions.x/2, 0, Math.PI*2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); map.parent.appendChild(this.div); this.draw(map); } com.modestmaps.CabFollower.prototype = { div: null, coord: null, offset: null, dimensions: null, margin: null, draw: function(map) { try { var point = map.coordinatePoint(this.coord); } catch(e) { // too soon? return; } if(point.x + this.dimensions.x + this.offset.x < 0) { // too far left this.div.style.display = 'none'; } else if(point.y + this.dimensions.y + this.offset.y < 0) { // too far up this.div.style.display = 'none'; } else if(point.x + this.offset.x > map.dimensions.x) { // too far right this.div.style.display = 'none'; } else if(point.y + this.offset.y > map.dimensions.y) { // too far down this.div.style.display = 'none'; } else { this.div.style.display = 'block'; this.div.style.left = point.x + this.offset.x + 'px'; this.div.style.top = point.y + this.offset.y + 'px'; } }, }; stamen-modestmaps-js-52d7710/examples/getbbox/0000775000175000017500000000000011653546654020271 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/getbbox/index.html0000664000175000017500000001464111653546654022274 0ustar daviddavid Get Bounding Box

Get Bounding Box

Shift-click and drag to draw a bounding box.
 

Click and drag to pan, mouse-wheel to zoom. Shift-click and drag to draw a bounding box.

Powered by Modest Maps JS. Map imagery CC-BY-SA OpenStreetMap.org contributors, used with reference to the OSM tile usage policy.

stamen-modestmaps-js-52d7710/examples/hello-oakland/0000775000175000017500000000000011653546654021351 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/hello-oakland/index.html0000664000175000017500000000707511653546654023357 0ustar daviddavid Modest Maps JS - Hello Oakland
stamen-modestmaps-js-52d7710/examples/markerclip/0000775000175000017500000000000011653546654020770 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/markerclip/index.html0000664000175000017500000000734611653546654022777 0ustar daviddavid Modest Maps JS Marker Demo

Modest Maps JS Marker Demo

stamen-modestmaps-js-52d7710/examples/greatcircle/0000775000175000017500000000000011653546654021123 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/greatcircle/index.html0000664000175000017500000000340211653546654023117 0ustar daviddavid Modest Maps JS - Great Circle Test stamen-modestmaps-js-52d7710/examples/twomaps/0000775000175000017500000000000011653546654020331 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/twomaps/index.html0000664000175000017500000000521411653546654022330 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

stamen-modestmaps-js-52d7710/examples/cloudmade/0000775000175000017500000000000011653546654020574 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/cloudmade/cloudmade.js0000664000175000017500000000146611653546654023076 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } com.modestmaps.CloudMadeProvider = function(key, style) { this.key = key; this.style = style; } com.modestmaps.CloudMadeProvider.prototype = { key: null, style: null, getTileUrl: function(coord) { coord = this.sourceCoordinate(coord); var worldSize = Math.pow(2, coord.zoom); var server = new Array('a.', 'b.', 'c.', '')[parseInt(worldSize * coord.row + coord.column) % 4]; var imgPath = new Array(this.key, this.style, this.tileWidth, coord.zoom, coord.column, coord.row).join('/'); return 'http://' + server + 'tile.cloudmade.com/' + imgPath + '.png'; } } com.modestmaps.extend(com.modestmaps.CloudMadeProvider, com.modestmaps.MapProvider); stamen-modestmaps-js-52d7710/examples/cloudmade/cloudmade.html0000664000175000017500000000246011653546654023421 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

The above div is a map initialized like so:

// "import" the namespace
var MM = com.modestmaps;

// please use your own API key, see http://developers.cloudmade.com/ for more details
var provider = new MM.CloudMadeProvider('1a914755a77758e49e19a26e799268b7','998');

map = new MM.Map('map', provider, new MM.Point(640,480))

map.setCenterZoom(new MM.Location(37.804656, -122.263606), 14);

Hands up who wants overlays?

stamen-modestmaps-js-52d7710/examples/cloudmade/full.html0000664000175000017500000000142711653546654022430 0ustar daviddavid Modest Maps JS stamen-modestmaps-js-52d7710/examples/polar/0000775000175000017500000000000011653546654017754 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/polar/polar.js0000664000175000017500000001070511653546654021432 0ustar daviddavid// THe following values were obtained http://nsidc.org/data/atlas/epsg_3411.html // North var la0=-45; // Central var phi1=90; // Data from http://nsidc.org/data/atlas/epsg_3411.html var phic=70; // optional var a=6378273.0; // Hughes ellipsoid required here http://nsidc.org/data/polar_stereo/ps_grids.html var e=0.081816153; var k0=1; // Scale factor function unproject(x, y) { var degRad=Math.PI / 180; var radDeg=180 / Math.PI; // Equation 14-15 var m1=Math.cos(phi1 * degRad) / Math.pow((1 - Math.pow(e, 2) * Math.pow(Math.sin(phi1 * degRad), 2)), 1 / 2); // Equation 3-1 var X1=2 * Math.atan(Math.tan((45 + phi1 / 2) * degRad) * Math.pow((1 - e * Math.sin(phi1 * degRad)) / (1 + e * Math.sin(phi1 * degRad)), e / 2)) - Math.PI / 2; // Equations 21-18 / 38 var p=Math.pow(Math.pow(x, 2) + Math.pow(y, 2), 1 / 2); var ce=2 * Math.atan(p * Math.cos(X1) / (2 * a * k0 * m1)); //Equation 21-37 var X=Math.asin(Math.cos(ce) * Math.sin(X1) + (y * Math.sin(ce) * Math.cos(X1) / p)); // Equation 3-4 // Using X as the first trial for phi var phi=2 * Math.atan(Math.tan(Math.PI / 4 + X / 2) * Math.pow((1 + e * Math.sin(X)) / (1 - e * Math.sin(X)), e / 2)) - Math.PI / 2; // Using this new trial value for phi, not for X phi=2 * Math.atan(Math.tan(Math.PI / 4 + X / 2) * Math.pow((1 + e * Math.sin(phi)) / (1 - e * Math.sin(phi)), e / 2)) - Math.PI / 2; phi=(2 * Math.atan(Math.tan(Math.PI / 4 + X / 2) * Math.pow((1 + e * Math.sin(phi)) / (1 - e * Math.sin(phi)), e / 2)) - Math.PI / 2) * radDeg; // Equation 21-36 // The next trial calculation produces the same phi to seven decimals. Therefore, this is phi. var la= la0 + Math.atan(x * Math.sin(ce) / (p * Math.cos(X1) * Math.cos(ce) - y * Math.sin(X1) * Math.sin(ce))) * radDeg; // If the denominator of the arctan argument is negative (reversed in Snyder), it is necessary to add or subtract 180, // whicever will place final la between +180 and -180. var arctanDenominator = (p * Math.cos(X1) * Math.cos(ce) - y * Math.sin(X1) * Math.sin(ce)); //Reverse coordinates if(arctanDenominator < 0) { if (la < 0) la += 180; else if(la > 0) la -= 180; } return { x: la, y: phi }; } // la = lon; phi = lat function project(phi, la) { var degRad=Math.PI / 180; var radDeg=180 / Math.PI // Equation 3-1 var X1=2 * Math.atan(Math.tan((45 + phi1 / 2) * degRad) * Math.pow((1 - e * Math.sin(phi1 * degRad)) / (1 + e * Math.sin(phi1 * degRad)), e / 2)) - Math.PI / 2; var X=2 * Math.atan(Math.tan((45 + phi / 2) * degRad) * Math.pow((1 - e * Math.sin(phi * degRad)) / (1 + e * Math.sin(phi * degRad)), e / 2)) - Math.PI / 2; // Equation 14-15 var m1=Math.cos(phi1 * degRad) / Math.pow((1 - Math.pow(e, 2) * Math.pow(Math.sin(phi1 * degRad), 2)), 1 / 2); var m=Math.cos(phi * degRad) / Math.pow((1 - Math.pow(e, 2) * Math.pow(Math.sin(phi * degRad), 2)), 1 / 2); if (phi1 == 90) { // Equation 15-9 var t = Math.tan((45-phi/2)*degRad) / Math.pow( (1-e*Math.sin(phi*degRad))/(1+e*Math.sin(phi*degRad)), e/2); var p; if (phic) { var tc = Math.tan((45-phic/2)*degRad) / Math.pow( (1-e*Math.sin(phic*degRad))/(1+e*Math.sin(phic*degRad)), e/2); var mc = Math.cos(phic * degRad) / Math.pow((1 - Math.pow(e, 2) * Math.pow(Math.sin(phic * degRad), 2)), 1 / 2); p = a*mc*t/tc; } else { // Equation 21-33 p = 2 * a * k0 * t / Math.pow( Math.pow(1+e,1+e)*Math.pow(1-e,1-e), 1/2 ); } // Equations 21-30 / 31 / 32 var x = p * Math.sin((la - la0) * degRad); var y = -p * Math.cos((la - la0) * degRad); var k = p / (a * m); return { x: x, y: y }; } else { // Equation 21-27 var A=2 * a * k0 * m1 / (Math.cos(X1) * (1 + Math.sin(X1) * Math.sin(X) + Math.cos(X1) * Math.cos(X) * Math.cos((la - la0) * degRad))); //console.log(A); // expecting 64501707.7 // Equations 21-24 / 25 / 26 var x=A * Math.cos(X) * Math.sin((la - la0) * degRad); var y=A * (Math.cos(X1) * Math.sin(X) - Math.sin(X1) * Math.cos(X) * Math.cos((la - la0) * degRad)); var k=A * Math.cos(X) / (a * m); //console.log('x', x); // expecting 971630.8 //console.log('y', y); // -1063049.3 //console.log('k', k); return { x: x, y: y }; } } stamen-modestmaps-js-52d7710/examples/node/0000775000175000017500000000000011653546654017564 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/node/.gitignore0000664000175000017500000000001011653546654021543 0ustar daviddavidmap.png stamen-modestmaps-js-52d7710/examples/node/modestmaps-static.js0000664000175000017500000000674311653546654023575 0ustar daviddavidvar MM = require('../../modestmaps.js'), Canvas = require('canvas'), Image = Canvas.Image; get = require('get'), express = require('express'), fs = require('fs'); function renderStaticMap(provider, dimensions, zoom, location, callback) { var canvas = new Canvas(dimensions.x, dimensions.y), ctx = canvas.getContext('2d'); var centerCoordinate = provider.locationCoordinate(location).zoomTo(zoom); function pointCoordinate(point) { // new point coordinate reflecting distance from map center, in tile widths var coord = centerCoordinate.copy(); coord.column += (point.x - dimensions.x/2) / provider.tileWidth; coord.row += (point.y - dimensions.y/2) / provider.tileHeight; return coord; }; function coordinatePoint(coord) { // Return an x, y point on the map image for a given coordinate. if (coord.zoom != zoom) { coord = coord.zoomTo(zoom); } var point = new MM.Point(dimensions.x/2, dimensions.y/2); point.x += provider.tileWidth * (coord.column - centerCoordinate.column); point.y += provider.tileHeight * (coord.row - centerCoordinate.row); return point; } var startCoord = pointCoordinate(new MM.Point(0,0)).container(), endCoord = pointCoordinate(dimensions).container(); var numRequests = 0, completeRequests = 0; function checkDone() { if (completeRequests == numRequests) { callback(null, canvas); } } function getTile(url, p) { new get(url).asBuffer(function(error,data) { if (error) { callback(url + ' error: ' + error); } else { var img = new Image(); img.src = data; ctx.drawImage(img, p.x, p.y, provider.tileWidth, provider.tileHeight); completeRequests++; checkDone(); } }); } for (var column = startCoord.column; column <= endCoord.column; column++) { for (var row = startCoord.row; row <= endCoord.row; row++) { var c = new MM.Coordinate(row, column, zoom), url = provider.getTileUrl(c), p = coordinatePoint(c); if (url) { getTile(url, p); numRequests++; } } } } /* var provider = new MM.TemplatedMapProvider("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png"); var dimensions = new MM.Point(800, 600); var zoom = 11; var location = new MM.Location(37.774929, -122.419415); renderStaticMap(provider, dimensions, 11, location, function(err, canvas) { if (err) { throw err; } var out = fs.createWriteStream(__dirname + '/map.png'), stream = canvas.createPNGStream(); stream.on('data', function(chunk){ out.write(chunk); }); stream.on('end', function(){ console.log('saved map.png'); }); }); */ // just one for now... var providers = { osm: new MM.TemplatedMapProvider("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png") } var app = express.createServer(); app.get('/map', function(req,res) { var provider = providers[req.param("provider", "osm")], // default osm width = req.param("width", 800), height = req.param("height", 600), dimensions = new MM.Point(width, height), zoom = parseInt(req.param("zoom", 1)), lat = req.param("lat", 0.0), lon = req.param("lon", 0.0), location = new MM.Location(lat, lon); renderStaticMap(provider, dimensions, zoom, location, function(err,canvas) { if (err) { res.send(new Error(err)); } else { res.header('Content-Type', 'image/png'); res.send(canvas.toBuffer()); } }); }); app.listen(3000); stamen-modestmaps-js-52d7710/examples/node/README0000664000175000017500000000064711653546654020453 0ustar daviddavidThis example works with node.js and (ideally) npm. The included package.json file means you should be able to type `npm install` in this folder to pull down all dependencies. Then do `node modestmaps-static.js` to run the example server. Example URL, San Francisco at zoom level 11 on OpenStreetMap, in an 800x600 image: http://localhost:3000/map?provider=osm&width=800&height=600&zoom=11&lat=37.774929&lon=-122.419415 stamen-modestmaps-js-52d7710/examples/node/package.json0000664000175000017500000000105511653546654022053 0ustar daviddavid{ "author": "Tom Carden (http://www.tom-carden.co.uk)", "name": "modestmaps-static", "description": "create a static image composed of several map tiles", "version": "0.0.0", "homepage": "https://github.com/stamen/modestmaps-js/", "repository": { "type": "git", "url": "git://github.com/stamen/modestmaps-js.git" }, "main": "modestmaps-static.js", "engines": { "node": "~v0.4.2" }, "dependencies": { "express": "~2.4.3", "canvas": "~0.7.0", "get": "~0.4.0" }, "devDependencies": {} } stamen-modestmaps-js-52d7710/examples/tilecache/0000775000175000017500000000000011653546654020560 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/tilecache/tilecache.js0000664000175000017500000000322111653546654023035 0ustar daviddavidcom.modestmaps.TileCacheMapProvider = function(template, subdomains) { // utility functions... function addZeros(i, zeros) { if (zeros === undefined) { zeros = 3; } var s = i.toString(); while (s.length < zeros) { s = '0' + s; } return s; } function tilePad(i) { return addZeros(parseInt(i / 1000000)) + '/' + addZeros(parseInt(i / 1000)) + '/' + addZeros(i % 1000); } // this is like a call to 'super', kinda... // we end up initializing a basic modestmaps.js MapProvider with // a getTileURL function that knows about the above utility functions com.modestmaps.MapProvider.call(this, function(coord) { coord = this.sourceCoordinate(coord); if (!coord) { return null; } var mod = coord.copy(); // fill in coordinates into URL var url = template.replace('{Z}', mod.zoom) .replace('{X}', tilePad(mod.column)) .replace('{Y}', tilePad(Math.pow(2, mod.zoom) - 1 - mod.row)); // replace the {S} portion of the url with the appropriate subdomain if (url.indexOf('{S}') > -1) { var subdomain = (subdomains && subdomains.length > 0) ? subdomains[parseInt(mod.row + mod.column) % subdomains.length] : ''; url = url.replace('{S}', subdomain ? subdomain + '.' : ''); } return url; }); }; com.modestmaps.extend(com.modestmaps.TileCacheMapProvider, com.modestmaps.MapProvider); stamen-modestmaps-js-52d7710/examples/tilecache/index.html0000664000175000017500000000175611653546654022566 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

stamen-modestmaps-js-52d7710/examples/anyscale/0000775000175000017500000000000011653546654020436 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/anyscale/index.html0000664000175000017500000000246211653546654022437 0ustar daviddavid Modest Maps JS - Any Zoom Demo
stamen-modestmaps-js-52d7710/examples/anyscale/anyzoom.js0000664000175000017500000001151611653546654022474 0ustar daviddavid// TODO: namespace/anonymous function var AnyZoomHandler = function(map) { if (map !== undefined) { this.init(map); } this.last = 0; }; // https://bugs.webkit.org/show_bug.cgi?id=40441 var bug40441 = /WebKit\/533/.test(navigator.userAgent) ? -1 : 0; AnyZoomHandler.prototype = { init: function(map) { this.map = map; com.modestmaps.addEvent(map.layerParent, 'dblclick', this.getDoubleClick()); com.modestmaps.addEvent(map.layerParent, 'mousedown', this.getMouseDown()); com.modestmaps.addEvent(map.parent, 'mousewheel', this.getMouseWheel()); }, mouseDownHandler: null, getMouseDown: function() { if (!this.mouseDownHandler) { var theHandler = this; this.mouseDownHandler = function(e) { com.modestmaps.addEvent(document, 'mouseup', theHandler.getMouseUp()); com.modestmaps.addEvent(document, 'mousemove', theHandler.getMouseMove()); theHandler.prevMouse = new com.modestmaps.Point(e.clientX, e.clientY); theHandler.map.parent.style.cursor = 'move'; return com.modestmaps.cancelEvent(e); }; } return this.mouseDownHandler; }, mouseMoveHandler: null, getMouseMove: function() { if (!this.mouseMoveHandler) { var theHandler = this; this.mouseMoveHandler = function(e) { if (theHandler.prevMouse) { theHandler.map.panBy(e.clientX - theHandler.prevMouse.x, e.clientY - theHandler.prevMouse.y); theHandler.prevMouse.x = e.clientX; theHandler.prevMouse.y = e.clientY; } return com.modestmaps.cancelEvent(e); }; } return this.mouseMoveHandler; }, mouseUpHandler: null, getMouseUp: function() { if (!this.mouseUpHandler) { var theHandler = this; this.mouseUpHandler = function(e) { com.modestmaps.removeEvent(document, 'mouseup', theHandler.getMouseUp()); com.modestmaps.removeEvent(document, 'mousemove', theHandler.getMouseMove()); theHandler.prevMouse = null; theHandler.map.parent.style.cursor = ''; return com.modestmaps.cancelEvent(e); }; } return this.mouseUpHandler; }, mouseWheelHandler: null, getMouseWheel: function() { if (!this.mouseWheelHandler) { var theHandler = this; this.mouseWheelHandler = function(e) { var delta = 0; if (e.wheelDelta) { delta = e.wheelDelta / 120; } else if (e.detail) { delta = -e.detail; } delta *= 0.1; /* Detect fast & large wheel events on WebKit. */ if (bug40441 < 0) { var now = new Date().getTime(), since = now - this.last; if ((since > 9) && (Math.abs(e.wheelDelta) / since >= 50)) bug40441 = 1; this.last = now; } if (bug40441 == 1) delta *= .03; var point = theHandler.getMousePoint(e); theHandler.map.zoomByAbout(delta, point); //Math.min(0.5, Math.max(-0.5, delta/10.0)), point); return com.modestmaps.cancelEvent(e); }; } return this.mouseWheelHandler; }, doubleClickHandler: null, getDoubleClick: function() { if (!this.doubleClickHandler) { var theHandler = this; this.doubleClickHandler = function(e) { var point = theHandler.getMousePoint(e); // use shift-double-click to zoom out theHandler.map.zoomByAbout(e.shiftKey ? -1 : 1, point); return com.modestmaps.cancelEvent(e); }; } return this.doubleClickHandler; }, // interaction helper getMousePoint: function(e) { // start with just the mouse (x, y) var point = new com.modestmaps.Point(e.clientX, e.clientY); // correct for scrolled document point.x += document.body.scrollLeft + document.documentElement.scrollLeft; point.y += document.body.scrollTop + document.documentElement.scrollTop; // correct for nested offsets in DOM for(var node = this.map.parent; node; node = node.offsetParent) { point.x -= node.offsetLeft; point.y -= node.offsetTop; } return point; } }; stamen-modestmaps-js-52d7710/examples/zoompan/0000775000175000017500000000000011661034367020312 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/zoompan/index.html0000664000175000017500000001567711653546654022337 0ustar daviddavid Modest Maps JS - Smooth Efficient Zooming and Panning
stamen-modestmaps-js-52d7710/examples/bubble/0000775000175000017500000000000011653546654020072 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/bubble/excanvas.js0000775000175000017500000005673311653546654022261 0ustar daviddavid// Copyright 2006 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Known Issues: // // * Patterns are not implemented. // * Radial gradient are not implemented. The VML version of these look very // different from the canvas one. // * Clipping paths are not implemented. // * Coordsize. The width and height attribute have higher priority than the // width and height style values which isn't correct. // * Painting mode isn't implemented. // * Canvas width/height should is using content-box by default. IE in // Quirks mode will draw the canvas using border-box. Either change your // doctype to HTML5 // (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) // or use Box Sizing Behavior from WebFX // (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) // * Optimize. There is always room for speed improvements. // only add this code if we do not already have a canvas implementation if (!window.CanvasRenderingContext2D) { (function () { // alias some functions to make (compiled) code shorter var m = Math; var mr = m.round; var ms = m.sin; var mc = m.cos; // this is used for sub pixel precision var Z = 10; var Z2 = Z / 2; var G_vmlCanvasManager_ = { init: function (opt_doc) { var doc = opt_doc || document; if (/MSIE/.test(navigator.userAgent) && !window.opera) { var self = this; doc.attachEvent("onreadystatechange", function () { self.init_(doc); }); } }, init_: function (doc) { if (doc.readyState == "complete") { // create xmlns if (!doc.namespaces["g_vml_"]) { doc.namespaces.add("g_vml_", "urn:schemas-microsoft-com:vml"); } // setup default css var ss = doc.createStyleSheet(); ss.cssText = "canvas{display:inline-block;overflow:hidden;" + // default size is 300x150 in Gecko and Opera "text-align:left;width:300px;height:150px}" + "g_vml_\\:*{behavior:url(#default#VML)}"; // find all canvas elements var els = doc.getElementsByTagName("canvas"); for (var i = 0; i < els.length; i++) { if (!els[i].getContext) { this.initElement(els[i]); } } } }, fixElement_: function (el) { // in IE before version 5.5 we would need to add HTML: to the tag name // but we do not care about IE before version 6 var outerHTML = el.outerHTML; var newEl = el.ownerDocument.createElement(outerHTML); // if the tag is still open IE has created the children as siblings and // it has also created a tag with the name "/FOO" if (outerHTML.slice(-2) != "/>") { var tagName = "/" + el.tagName; var ns; // remove content while ((ns = el.nextSibling) && ns.tagName != tagName) { ns.removeNode(); } // remove the incorrect closing tag if (ns) { ns.removeNode(); } } el.parentNode.replaceChild(newEl, el); return newEl; }, /** * Public initializes a canvas element so that it can be used as canvas * element from now on. This is called automatically before the page is * loaded but if you are creating elements using createElement you need to * make sure this is called on the element. * @param {HTMLElement} el The canvas element to initialize. * @return {HTMLElement} the element that was created. */ initElement: function (el) { el = this.fixElement_(el); el.getContext = function () { if (this.context_) { return this.context_; } return this.context_ = new CanvasRenderingContext2D_(this); }; // do not use inline function because that will leak memory el.attachEvent('onpropertychange', onPropertyChange); el.attachEvent('onresize', onResize); var attrs = el.attributes; if (attrs.width && attrs.width.specified) { // TODO: use runtimeStyle and coordsize // el.getContext().setWidth_(attrs.width.nodeValue); el.style.width = attrs.width.nodeValue + "px"; } else { el.width = el.clientWidth; } if (attrs.height && attrs.height.specified) { // TODO: use runtimeStyle and coordsize // el.getContext().setHeight_(attrs.height.nodeValue); el.style.height = attrs.height.nodeValue + "px"; } else { el.height = el.clientHeight; } //el.getContext().setCoordsize_() return el; } }; function onPropertyChange(e) { var el = e.srcElement; switch (e.propertyName) { case 'width': el.style.width = el.attributes.width.nodeValue + "px"; el.getContext().clearRect(); break; case 'height': el.style.height = el.attributes.height.nodeValue + "px"; el.getContext().clearRect(); break; } } function onResize(e) { var el = e.srcElement; if (el.firstChild) { el.firstChild.style.width = el.clientWidth + 'px'; el.firstChild.style.height = el.clientHeight + 'px'; } } G_vmlCanvasManager_.init(); // precompute "00" to "FF" var dec2hex = []; for (var i = 0; i < 16; i++) { for (var j = 0; j < 16; j++) { dec2hex[i * 16 + j] = i.toString(16) + j.toString(16); } } function createMatrixIdentity() { return [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ]; } function matrixMultiply(m1, m2) { var result = createMatrixIdentity(); for (var x = 0; x < 3; x++) { for (var y = 0; y < 3; y++) { var sum = 0; for (var z = 0; z < 3; z++) { sum += m1[x][z] * m2[z][y]; } result[x][y] = sum; } } return result; } function copyState(o1, o2) { o2.fillStyle = o1.fillStyle; o2.lineCap = o1.lineCap; o2.lineJoin = o1.lineJoin; o2.lineWidth = o1.lineWidth; o2.miterLimit = o1.miterLimit; o2.shadowBlur = o1.shadowBlur; o2.shadowColor = o1.shadowColor; o2.shadowOffsetX = o1.shadowOffsetX; o2.shadowOffsetY = o1.shadowOffsetY; o2.strokeStyle = o1.strokeStyle; o2.arcScaleX_ = o1.arcScaleX_; o2.arcScaleY_ = o1.arcScaleY_; } function processStyle(styleString) { var str, alpha = 1; styleString = String(styleString); if (styleString.substring(0, 3) == "rgb") { var start = styleString.indexOf("(", 3); var end = styleString.indexOf(")", start + 1); var guts = styleString.substring(start + 1, end).split(","); str = "#"; for (var i = 0; i < 3; i++) { str += dec2hex[Number(guts[i])]; } if ((guts.length == 4) && (styleString.substr(3, 1) == "a")) { alpha = guts[3]; } } else { str = styleString; } return [str, alpha]; } function processLineCap(lineCap) { switch (lineCap) { case "butt": return "flat"; case "round": return "round"; case "square": default: return "square"; } } /** * This class implements CanvasRenderingContext2D interface as described by * the WHATWG. * @param {HTMLElement} surfaceElement The element that the 2D context should * be associated with */ function CanvasRenderingContext2D_(surfaceElement) { this.m_ = createMatrixIdentity(); this.mStack_ = []; this.aStack_ = []; this.currentPath_ = []; // Canvas context properties this.strokeStyle = "#000"; this.fillStyle = "#000"; this.lineWidth = 1; this.lineJoin = "miter"; this.lineCap = "butt"; this.miterLimit = Z * 1; this.globalAlpha = 1; this.canvas = surfaceElement; var el = surfaceElement.ownerDocument.createElement('div'); el.style.width = surfaceElement.clientWidth + 'px'; el.style.height = surfaceElement.clientHeight + 'px'; el.style.overflow = 'hidden'; el.style.position = 'absolute'; surfaceElement.appendChild(el); this.element_ = el; this.arcScaleX_ = 1; this.arcScaleY_ = 1; }; var contextPrototype = CanvasRenderingContext2D_.prototype; contextPrototype.clearRect = function() { this.element_.innerHTML = ""; this.currentPath_ = []; }; contextPrototype.beginPath = function() { // TODO: Branch current matrix so that save/restore has no effect // as per safari docs. this.currentPath_ = []; }; contextPrototype.moveTo = function(aX, aY) { this.currentPath_.push({type: "moveTo", x: aX, y: aY}); this.currentX_ = aX; this.currentY_ = aY; }; contextPrototype.lineTo = function(aX, aY) { this.currentPath_.push({type: "lineTo", x: aX, y: aY}); this.currentX_ = aX; this.currentY_ = aY; }; contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, aCP2x, aCP2y, aX, aY) { this.currentPath_.push({type: "bezierCurveTo", cp1x: aCP1x, cp1y: aCP1y, cp2x: aCP2x, cp2y: aCP2y, x: aX, y: aY}); this.currentX_ = aX; this.currentY_ = aY; }; contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { // the following is lifted almost directly from // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes var cp1x = this.currentX_ + 2.0 / 3.0 * (aCPx - this.currentX_); var cp1y = this.currentY_ + 2.0 / 3.0 * (aCPy - this.currentY_); var cp2x = cp1x + (aX - this.currentX_) / 3.0; var cp2y = cp1y + (aY - this.currentY_) / 3.0; this.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, aX, aY); }; contextPrototype.arc = function(aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise) { aRadius *= Z; var arcType = aClockwise ? "at" : "wa"; var xStart = aX + (mc(aStartAngle) * aRadius) - Z2; var yStart = aY + (ms(aStartAngle) * aRadius) - Z2; var xEnd = aX + (mc(aEndAngle) * aRadius) - Z2; var yEnd = aY + (ms(aEndAngle) * aRadius) - Z2; // IE won't render arches drawn counter clockwise if xStart == xEnd. if (xStart == xEnd && !aClockwise) { xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something // that can be represented in binary } this.currentPath_.push({type: arcType, x: aX, y: aY, radius: aRadius, xStart: xStart, yStart: yStart, xEnd: xEnd, yEnd: yEnd}); }; contextPrototype.rect = function(aX, aY, aWidth, aHeight) { this.moveTo(aX, aY); this.lineTo(aX + aWidth, aY); this.lineTo(aX + aWidth, aY + aHeight); this.lineTo(aX, aY + aHeight); this.closePath(); }; contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { // Will destroy any existing path (same as FF behaviour) this.beginPath(); this.moveTo(aX, aY); this.lineTo(aX + aWidth, aY); this.lineTo(aX + aWidth, aY + aHeight); this.lineTo(aX, aY + aHeight); this.closePath(); this.stroke(); }; contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { // Will destroy any existing path (same as FF behaviour) this.beginPath(); this.moveTo(aX, aY); this.lineTo(aX + aWidth, aY); this.lineTo(aX + aWidth, aY + aHeight); this.lineTo(aX, aY + aHeight); this.closePath(); this.fill(); }; contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { var gradient = new CanvasGradient_("gradient"); return gradient; }; contextPrototype.createRadialGradient = function(aX0, aY0, aR0, aX1, aY1, aR1) { var gradient = new CanvasGradient_("gradientradial"); gradient.radius1_ = aR0; gradient.radius2_ = aR1; gradient.focus_.x = aX0; gradient.focus_.y = aY0; return gradient; }; contextPrototype.drawImage = function (image, var_args) { var dx, dy, dw, dh, sx, sy, sw, sh; // to find the original width we overide the width and height var oldRuntimeWidth = image.runtimeStyle.width; var oldRuntimeHeight = image.runtimeStyle.height; image.runtimeStyle.width = 'auto'; image.runtimeStyle.height = 'auto'; // get the original size var w = image.width; var h = image.height; // and remove overides image.runtimeStyle.width = oldRuntimeWidth; image.runtimeStyle.height = oldRuntimeHeight; if (arguments.length == 3) { dx = arguments[1]; dy = arguments[2]; sx = sy = 0; sw = dw = w; sh = dh = h; } else if (arguments.length == 5) { dx = arguments[1]; dy = arguments[2]; dw = arguments[3]; dh = arguments[4]; sx = sy = 0; sw = w; sh = h; } else if (arguments.length == 9) { sx = arguments[1]; sy = arguments[2]; sw = arguments[3]; sh = arguments[4]; dx = arguments[5]; dy = arguments[6]; dw = arguments[7]; dh = arguments[8]; } else { throw "Invalid number of arguments"; } var d = this.getCoords_(dx, dy); var w2 = sw / 2; var h2 = sh / 2; var vmlStr = []; var W = 10; var H = 10; // For some reason that I've now forgotten, using divs didn't work vmlStr.push(' ' , '', ''); this.element_.insertAdjacentHTML("BeforeEnd", vmlStr.join("")); }; contextPrototype.stroke = function(aFill) { var lineStr = []; var lineOpen = false; var a = processStyle(aFill ? this.fillStyle : this.strokeStyle); var color = a[0]; var opacity = a[1] * this.globalAlpha; var W = 10; var H = 10; lineStr.push(' max.x) { max.x = c.x; } if (min.y == null || c.y < min.y) { min.y = c.y; } if (max.y == null || c.y > max.y) { max.y = c.y; } } } lineStr.push(' ">'); if (typeof this.fillStyle == "object") { var focus = {x: "50%", y: "50%"}; var width = (max.x - min.x); var height = (max.y - min.y); var dimension = (width > height) ? width : height; focus.x = mr((this.fillStyle.focus_.x / width) * 100 + 50) + "%"; focus.y = mr((this.fillStyle.focus_.y / height) * 100 + 50) + "%"; var colors = []; // inside radius (%) if (this.fillStyle.type_ == "gradientradial") { var inside = (this.fillStyle.radius1_ / dimension * 100); // percentage that outside radius exceeds inside radius var expansion = (this.fillStyle.radius2_ / dimension * 100) - inside; } else { var inside = 0; var expansion = 100; } var insidecolor = {offset: null, color: null}; var outsidecolor = {offset: null, color: null}; // We need to sort 'colors' by percentage, from 0 > 100 otherwise ie // won't interpret it correctly this.fillStyle.colors_.sort(function (cs1, cs2) { return cs1.offset - cs2.offset; }); for (var i = 0; i < this.fillStyle.colors_.length; i++) { var fs = this.fillStyle.colors_[i]; colors.push( (fs.offset * expansion) + inside, "% ", fs.color, ","); if (fs.offset > insidecolor.offset || insidecolor.offset == null) { insidecolor.offset = fs.offset; insidecolor.color = fs.color; } if (fs.offset < outsidecolor.offset || outsidecolor.offset == null) { outsidecolor.offset = fs.offset; outsidecolor.color = fs.color; } } colors.pop(); lineStr.push(''); } else if (aFill) { lineStr.push(''); } else { lineStr.push( '' ); } lineStr.push(""); this.element_.insertAdjacentHTML("beforeEnd", lineStr.join("")); this.currentPath_ = []; }; contextPrototype.fill = function() { this.stroke(true); } contextPrototype.closePath = function() { this.currentPath_.push({type: "close"}); }; /** * @private */ contextPrototype.getCoords_ = function(aX, aY) { return { x: Z * (aX * this.m_[0][0] + aY * this.m_[1][0] + this.m_[2][0]) - Z2, y: Z * (aX * this.m_[0][1] + aY * this.m_[1][1] + this.m_[2][1]) - Z2 } }; contextPrototype.save = function() { var o = {}; copyState(this, o); this.aStack_.push(o); this.mStack_.push(this.m_); this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); }; contextPrototype.restore = function() { copyState(this.aStack_.pop(), this); this.m_ = this.mStack_.pop(); }; contextPrototype.translate = function(aX, aY) { var m1 = [ [1, 0, 0], [0, 1, 0], [aX, aY, 1] ]; this.m_ = matrixMultiply(m1, this.m_); }; contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { var m1 = [ [m11, m12, 0], [m21, m22, 0], [ dx, dy, 1] ]; this.m_ = matrixMultiply(m1, this.m_); }; contextPrototype.rotate = function(aRot) { var c = mc(aRot); var s = ms(aRot); var m1 = [ [c, s, 0], [-s, c, 0], [0, 0, 1] ]; this.m_ = matrixMultiply(m1, this.m_); }; contextPrototype.scale = function(aX, aY) { this.arcScaleX_ *= aX; this.arcScaleY_ *= aY; var m1 = [ [aX, 0, 0], [0, aY, 0], [0, 0, 1] ]; this.m_ = matrixMultiply(m1, this.m_); }; /******** STUBS ********/ contextPrototype.clip = function() { // TODO: Implement }; contextPrototype.arcTo = function() { // TODO: Implement }; contextPrototype.createPattern = function() { return new CanvasPattern_; }; // Gradient / Pattern Stubs function CanvasGradient_(aType) { this.type_ = aType; this.radius1_ = 0; this.radius2_ = 0; this.colors_ = []; this.focus_ = {x: 0, y: 0}; } CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { aColor = processStyle(aColor); this.colors_.push({offset: 1-aOffset, color: aColor}); }; function CanvasPattern_() {} // set up externs G_vmlCanvasManager = G_vmlCanvasManager_; CanvasRenderingContext2D = CanvasRenderingContext2D_; CanvasGradient = CanvasGradient_; CanvasPattern = CanvasPattern_; })(); } // if stamen-modestmaps-js-52d7710/examples/bubble/follower-canvas.js0000664000175000017500000001241111653546654023531 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } (function(MM) { MM.Follower = function(map, location, content) { this.coord = map.provider.locationCoordinate(location); this.offset = new MM.Point(0, 0); this.dimensions = new MM.Point(150, 150); this.margin = new MM.Point(10, 10); this.offset = new MM.Point(0, -this.dimensions.y); var follower = this; var callback = function(m, a) { return follower.draw(m); }; map.addCallback('drawn', callback); this.div = document.createElement('div'); this.div.style.position = 'absolute'; this.div.style.width = this.dimensions.x + 'px'; this.div.style.height = this.dimensions.y + 'px'; //this.div.style.backgroundColor = 'white'; //this.div.style.border = 'solid black 1px'; var shadow = document.createElement('canvas'); this.div.appendChild(shadow); if (typeof G_vmlCanvasManager !== 'undefined') shadow = G_vmlCanvasManager.initElement(shadow); shadow.style.position = 'absolute'; shadow.style.left = '0px'; shadow.style.top = '0px'; shadow.width = this.dimensions.x*2; shadow.height = this.dimensions.y; var ctx = shadow.getContext("2d"); ctx.transform(1, 0, -0.5, 0.5, 75, this.dimensions.y/2); ctx.fillStyle = "rgba(0,0,0,0.5)"; this.drawBubblePath(ctx); ctx.fill(); var bubble = document.createElement('canvas'); this.div.appendChild(bubble); if (typeof G_vmlCanvasManager !== 'undefined') bubble = G_vmlCanvasManager.initElement(bubble); bubble.style.position = 'absolute'; bubble.style.left = '0px'; bubble.style.top = '0px'; bubble.width = this.dimensions.x; bubble.height = this.dimensions.y; var bubCtx = bubble.getContext('2d'); bubCtx.strokeStyle = 'black'; bubCtx.fillStyle = 'white'; this.drawBubblePath(bubCtx); bubCtx.fill(); bubCtx.stroke(); var contentDiv = document.createElement('div'); contentDiv.style.position = 'absolute'; contentDiv.style.left = '0px'; contentDiv.style.top = '0px'; contentDiv.style.overflow = 'hidden'; contentDiv.style.width = (this.dimensions.x - this.margin.x) + 'px'; contentDiv.style.height = (this.dimensions.y - this.margin.y - 25) + 'px'; contentDiv.style.padding = this.margin.y + 'px ' + this.margin.x + 'px ' + this.margin.y + 'px ' + this.margin.x + 'px'; contentDiv.innerHTML = content; this.div.appendChild(contentDiv); MM.addEvent(contentDiv, 'mousedown', function(e) { if(!e) e = window.event; return MM.cancelEvent(e); }); map.parent.appendChild(this.div); this.draw(map); } MM.Follower.prototype = { div: null, coord: null, offset: null, dimensions: null, margin: null, draw: function(map) { console.log(Date.now()); try { var point = map.coordinatePoint(this.coord); } catch(e) { console.error(e); // too soon? return; } if(point.x + this.dimensions.x + this.offset.x < 0) { // too far left this.div.style.display = 'none'; } else if(point.y + this.dimensions.y + this.offset.y < 0) { // too far up this.div.style.display = 'none'; } else if(point.x + this.offset.x > map.dimensions.x) { // too far right this.div.style.display = 'none'; } else if(point.y + this.offset.y > map.dimensions.y) { // too far down this.div.style.display = 'none'; } else { this.div.style.display = 'block'; MM.moveElement(this.div, { x: Math.round(point.x + this.offset.x), y: Math.round(point.y + this.offset.y), scale: 1, width: this.dimensions.x, height: this.dimensions.y }); } }, drawBubblePath: function(ctx) { ctx.beginPath(); ctx.moveTo(10, this.dimensions.y); ctx.lineTo(35, this.dimensions.y-25); ctx.lineTo(this.dimensions.x-10, this.dimensions.y-25); ctx.quadraticCurveTo(this.dimensions.x, this.dimensions.y-25, this.dimensions.x, this.dimensions.y-35); ctx.lineTo(this.dimensions.x, 10); ctx.quadraticCurveTo(this.dimensions.x, 0, this.dimensions.x-10, 0); ctx.lineTo(10, 0); ctx.quadraticCurveTo(0, 0, 0, 10); ctx.lineTo(0, this.dimensions.y-35); ctx.quadraticCurveTo(0, this.dimensions.y-25, 10, this.dimensions.y-25); ctx.lineTo(15, this.dimensions.y-25); ctx.moveTo(10, this.dimensions.y); } }; })(com.modestmaps)stamen-modestmaps-js-52d7710/examples/bubble/canvas-bubble.html0000664000175000017500000000250211653546654023463 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

The above div is a map initialized like so:

// "import" the namespace
var mm = com.modestmaps;

var provider = new mm.TemplatedMapProvider('http://osm-bayarea.s3.amazonaws.com/{Z}-r{Y}-c{X}.jpg');

map = new mm.Map('map', provider, new mm.Point(1024,768))

var f = new mm.Follower(map, new mm.Location(37.811530, -122.2666097), 'Broadway and Grand');

map.setCenterZoom(new mm.Location(37.811530, -122.2666097), 14);

Hands up who wants overlays?

stamen-modestmaps-js-52d7710/examples/bubble/README0000664000175000017500000000014011653546654020745 0ustar daviddavidexcanvas is from http://code.google.com/p/explorercanvas/ and licensed under Apache License 2.0 stamen-modestmaps-js-52d7710/examples/simple/0000775000175000017500000000000011653546654020130 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/simple/minimal-maps.html0000664000175000017500000000125311653546654023403 0ustar daviddavid Modest Maps JS stamen-modestmaps-js-52d7710/examples/simple/index.html0000664000175000017500000000256011653546654022130 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

The above div is a map initialized like so:

// "import" the namespace
var MM = com.modestmaps;

var provider = new MM.MapProvider(function(coord) {
    var img = parseInt(coord.zoom) +'-r'+ parseInt(coord.row) +'-c'+ parseInt(coord.column) + '.jpg';
    return 'http://osm-bayarea.s3.amazonaws.com/' + img;
});

map = new MM.Map('map', provider, new MM.Point(900,900))

var f = new MM.Follower(map, new MM.Location(37.811530, -122.2666097), '° Broadway and Grand');

map.setCenterZoom(new MM.Location(37.811530, -122.2666097), 14);

Hands up who wants overlays?

stamen-modestmaps-js-52d7710/examples/simple/follower.js0000664000175000017500000000440211653546654022317 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } com.modestmaps.Follower = function(map, location, content, dimensions) { this.coord = map.provider.locationCoordinate(location); this.offset = new com.modestmaps.Point(0, 0); this.dimensions = dimensions || new com.modestmaps.Point(100, 50); this.padding = new com.modestmaps.Point(10, 10); this.offset = new com.modestmaps.Point(0, -this.dimensions.y); var follower = this; var callback = function(m, a) { return follower.draw(m); }; map.addCallback('drawn', callback); this.div = document.createElement('div'); this.div.style.position = 'absolute'; this.div.style.width = this.dimensions.x + 'px'; this.div.style.height = this.dimensions.y + 'px'; //this.div.style.backgroundColor = 'white'; //this.div.style.border = 'solid black 1px'; this.div.innerHTML = content; com.modestmaps.addEvent(this.div, 'mousedown', function(e) { if(!e) e = window.event; return com.modestmaps.cancelEvent(e); }); map.parent.appendChild(this.div); this.draw(map); } com.modestmaps.Follower.prototype = { div: null, coord: null, offset: null, dimensions: null, padding: null, draw: function(map) { try { var point = map.coordinatePoint(this.coord); } catch(e) { // too soon? return; } if(point.x + this.dimensions.x + this.offset.x < 0) { // too far left this.div.style.display = 'none'; } else if(point.y + this.dimensions.y + this.offset.y < 0) { // too far up this.div.style.display = 'none'; } else if(point.x + this.offset.x > map.dimensions.x) { // too far right this.div.style.display = 'none'; } else if(point.y + this.offset.y > map.dimensions.y) { // too far down this.div.style.display = 'none'; } else { this.div.style.display = 'block'; this.div.style.left = point.x + this.offset.x + 'px'; this.div.style.top = point.y + this.offset.y + 'px'; } } }; stamen-modestmaps-js-52d7710/examples/README0000664000175000017500000000115111653546654017515 0ustar daviddavidSome of Modest Maps JS is experimental, and under active development. These examples reflect this exploration, and occasionally use external libraries (like jquery, raphael, excanvas and dojo) that the core Modest Maps code does not depend on. Plans involve experimentation with the following: * SVG and Canvas drawing * Webkit/Gecko CSS transforms/animations * IE CSS filters. * arbitrary zoom levels * rotations * touch events * ipad/iphone/ipod/android support Contributions are welcome! Please contact us via github (file an issue or message @RandomEtc, @migurski or @tmcw) if you would like to get involved. stamen-modestmaps-js-52d7710/examples/microsoft/0000775000175000017500000000000011653546654020644 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/microsoft/bing.js0000664000175000017500000000473011653546654022125 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } com.modestmaps.BingProvider = function(key, style, onready) { this.key = key; this.style = style; // hit the imagery metadata service // http://msdn.microsoft.com/en-us/library/ff701716.aspx // Aerial, AerialWithLabels, Road var script = document.createElement('script'); script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); script.src = 'http://dev.virtualearth.net/REST/V1/Imagery/Metadata/'+style+'/?key='+key+'&jsonp=onBingComplete'; function toMicrosoft(column, row, zoom) { // generate zoom string by interleaving row/col bits // NB:- this assumes you're only asking for positive row/cols var quadKey = ""; for (var i = 1; i <= zoom; i++) { var rowBit = (row >> zoom-i) & 1; var colBit = (column >> zoom-i) & 1; quadKey += (rowBit << 1) + colBit; } return quadKey; } var provider = this; window.onBingComplete = function(data) { var resourceSets = data.resourceSets; for (var i = 0; i < resourceSets.length; i++) { var resources = data.resourceSets[i].resources; for (var j = 0; j < resources.length; j++) { var resource = resources[j]; var serverSalt = Math.floor(Math.random() * 4); provider.getTileUrl = function(coord) { var quadKey = toMicrosoft(coord.column, coord.row, coord.zoom); // this is so that requests will be consistent in this session, rather than totally random var server = Math.abs(serverSalt + coord.column + coord.row + coord.zoom) % 4; return resource.imageUrl .replace('{quadkey}',quadKey) .replace('{subdomain}', resource.imageUrlSubdomains[server]); } // TODO: use resource.imageWidth // TODO: use resource.imageHeight } } // TODO: display data.brandLogoUri // TODO: display data.copyright onready(provider); } } com.modestmaps.BingProvider.prototype = { key: null, style: null, subdomains: null, getTileUrl: null } com.modestmaps.extend(com.modestmaps.BingProvider, com.modestmaps.MapProvider); stamen-modestmaps-js-52d7710/examples/microsoft/index.html0000664000175000017500000000162511653546654022645 0ustar daviddavid Modest Maps JS stamen-modestmaps-js-52d7710/examples/templated/0000775000175000017500000000000011653546654020616 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/templated/index.html0000664000175000017500000000263311653546654022617 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

stamen-modestmaps-js-52d7710/examples/templated/follower.js0000664000175000017500000000465511653546654023017 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } com.modestmaps.Follower = function(map, location, content, dimensions) { this.coord = map.provider.locationCoordinate(location); this.offset = new com.modestmaps.Point(0, 0); this.dimensions = dimensions || new com.modestmaps.Point(100, 50); this.padding = new com.modestmaps.Point(10, 10); this.offset = new com.modestmaps.Point(0, -this.dimensions.y); var follower = this; var callback = function(m, a) { return follower.draw(m); }; map.addCallback('panned', callback); map.addCallback('zoomed', callback); map.addCallback('centered', callback); map.addCallback('extentset', callback); map.addCallback('resized', callback); this.div = document.createElement('div'); this.div.style.position = 'absolute'; this.div.style.width = this.dimensions.x + 'px'; this.div.style.height = this.dimensions.y + 'px'; //this.div.style.backgroundColor = 'white'; //this.div.style.border = 'solid black 1px'; this.div.innerHTML = content; com.modestmaps.addEvent(this.div, 'mousedown', function(e) { if(!e) e = window.event; return com.modestmaps.cancelEvent(e); }); map.parent.appendChild(this.div); this.draw(map); } com.modestmaps.Follower.prototype = { div: null, coord: null, offset: null, dimensions: null, padding: null, draw: function(map) { try { var point = map.coordinatePoint(this.coord); } catch(e) { // too soon? return; } if(point.x + this.dimensions.x + this.offset.x < 0) { // too far left this.div.style.display = 'none'; } else if(point.y + this.dimensions.y + this.offset.y < 0) { // too far up this.div.style.display = 'none'; } else if(point.x + this.offset.x > map.dimensions.x) { // too far right this.div.style.display = 'none'; } else if(point.y + this.offset.y > map.dimensions.y) { // too far down this.div.style.display = 'none'; } else { this.div.style.display = 'block'; this.div.style.left = point.x + this.offset.x + 'px'; this.div.style.top = point.y + this.offset.y + 'px'; } } }; stamen-modestmaps-js-52d7710/examples/zoombox/0000775000175000017500000000000011653546654020334 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/zoombox/zoombox.js0000664000175000017500000000621011653546654022366 0ustar daviddavidcom.modestmaps.ZoomBox = function(map) { this.map = map; var theBox = this; this.getMousePoint = function(e) { // start with just the mouse (x, y) var point = new com.modestmaps.Point(e.clientX, e.clientY); // correct for scrolled document point.x += document.body.scrollLeft + document.documentElement.scrollLeft; point.y += document.body.scrollTop + document.documentElement.scrollTop; // correct for nested offsets in DOM for(var node = this.map.parent; node; node = node.offsetParent) { point.x -= node.offsetLeft; point.y -= node.offsetTop; } return point; }; var boxDiv = document.createElement('div'); boxDiv.id = map.parent.id+'-zoombox'; boxDiv.style.cssText = 'margin:0; padding:0; position:absolute; top:0; left:0;' boxDiv.style.width = map.dimensions.x+'px'; boxDiv.style.height = map.dimensions.y+'px'; map.parent.appendChild(boxDiv); var box = document.createElement('div'); box.id = map.parent.id+'-zoombox-box'; box.style.cssText = 'margin:0; padding:0; border:1px dashed #888; background: rgba(255,255,255,0.25); position: absolute; top: 0; left: 0; width: 0; height: 0; display: none;'; boxDiv.appendChild(box); // TODO: respond to resize var mouseDownPoint = null; this.mouseDown = function(e) { if (e.shiftKey) { mouseDownPoint = theBox.getMousePoint(e); box.style.left = mouseDownPoint.x + 'px'; box.style.top = mouseDownPoint.y + 'px'; com.modestmaps.addEvent(map.parent, 'mousemove', theBox.mouseMove); com.modestmaps.addEvent(map.parent, 'mouseup', theBox.mouseUp); map.parent.style.cursor = 'crosshair'; return com.modestmaps.cancelEvent(e); } }; this.mouseMove = function(e) { var point = theBox.getMousePoint(e); box.style.display = 'block'; if (point.x < mouseDownPoint.x) { box.style.left = point.x + 'px'; } else { box.style.left = mouseDownPoint.x + 'px'; } box.style.width = Math.abs(point.x - mouseDownPoint.x) + 'px'; if (point.y < mouseDownPoint.y) { box.style.top = point.y + 'px'; } else { box.style.top = mouseDownPoint.y + 'px'; } box.style.height = Math.abs(point.y - mouseDownPoint.y) + 'px'; return com.modestmaps.cancelEvent(e); }; this.mouseUp = function(e) { var point = theBox.getMousePoint(e); var l1 = map.pointLocation(point); var l2 = map.pointLocation(mouseDownPoint); map.setExtent([l1,l2]); box.style.display = 'none'; com.modestmaps.removeEvent(map.parent, 'mousemove', theBox.mouseMove); com.modestmaps.removeEvent(map.parent, 'mouseup', theBox.mouseUp); map.parent.style.cursor = 'auto'; return com.modestmaps.cancelEvent(e); }; com.modestmaps.addEvent(boxDiv, 'mousedown', this.mouseDown); } stamen-modestmaps-js-52d7710/examples/zoombox/index.html0000664000175000017500000000151211653546654022330 0ustar daviddavid Modest Maps JS - Zoom Box
stamen-modestmaps-js-52d7710/examples/keyboard/0000775000175000017500000000000011653546654020437 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/keyboard/index.html0000664000175000017500000002026511653546654022441 0ustar daviddavid Modest Maps JS - Throwable Test

Click/drag to throw the map, or press and hold the arrow keys to fly around.

drag :
zoom :

Built with Modest Maps JS. Tiles Courtesy of MapQuest

stamen-modestmaps-js-52d7710/examples/show-hide/0000775000175000017500000000000011653546654020526 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/show-hide/index.html0000664000175000017500000000300211653546654022516 0ustar daviddavid display: none; example
toggle map visibility stamen-modestmaps-js-52d7710/examples/navwindow/0000775000175000017500000000000011653546654020653 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/navwindow/index.html0000664000175000017500000000336411653546654022656 0ustar daviddavid Modest Maps JS - Nav Window Test
stamen-modestmaps-js-52d7710/examples/flickr/0000775000175000017500000000000011653546654020111 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/flickr/flickr.html0000664000175000017500000000411311653546654022250 0ustar daviddavid Modest Maps JS

Modest Maps JS

zoom in | zoom out
pan left | pan right | pan down | pan up

The above div is a map initialized like so:

// "import" the namespace
var MM = com.modestmaps;

var provider = new MM.BlueMarbleProvider();

map = new MM.Map('map', provider, new MM.Point(1024,512))

map.setCenterZoom(new MM.Location(20.0, 0), 2);

window.jsonFlickrApi = function(rsp) {
    var photos = rsp.photos.photo;
    for (var i = 0; i < photos.length; i++) {
        var p = photos[i];
        var url = [ 'http://farm', p.farm, '.static.flickr.com/', p.server, '/', p.id, '_', p.secret, '_s.jpg' ].join('');    
        var html = "<" + "img src='" + url + "'" + ">"; // weird for 'eval', sorry
        var location = new MM.Location(p.latitude, p.longitude);
        var dimensions = new MM.Point(75, 75);
        var f = new MM.Follower(map, location, html, dimensions);
    }
}

var script = document.createElement('script');
script.src = 'http://api.flickr.com/services/rest/?'+
               'method=flickr.photos.search&'+
               'api_key=a96cbef093a8c0280d3aed4e0c004d4c&'+
               'tags=clouds&'+
               'extras=geo&'+
               'has_geo=1&'+
               'format=json';
document.getElementsByTagName('head')[0].appendChild(script);

Hands up who wants overlays?

stamen-modestmaps-js-52d7710/examples/flickr/bluemarble.js0000664000175000017500000000077111653546654022566 0ustar daviddavidcom.modestmaps.BlueMarbleProvider = function() { com.modestmaps.MapProvider.call(this, function(coordinate) { coordinate = this.sourceCoordinate(coordinate); if (!coordinate) return null; var img = coordinate.zoom.toFixed(0) +'-r'+ coordinate.row.toFixed(0) +'-c'+ coordinate.column.toFixed(0) + '.jpg'; return 'http://s3.amazonaws.com/com.modestmaps.bluemarble/' + img; }); }; com.modestmaps.extend(com.modestmaps.BlueMarbleProvider, com.modestmaps.MapProvider); stamen-modestmaps-js-52d7710/examples/flickr/follower.js0000664000175000017500000000453011653546654022302 0ustar daviddavid// namespacing! if (!com) { var com = { }; if (!com.modestmaps) { com.modestmaps = { }; } } (function(MM){ MM.Follower = function(map, location, content, dimensions) { this.coord = map.provider.locationCoordinate(location); this.offset = new MM.Point(0, 0); this.dimensions = dimensions || new MM.Point(100, 50); this.padding = new MM.Point(10, 10); this.offset = new MM.Point(0, -this.dimensions.y); var follower = this; var callback = function(m, a) { return follower.draw(m); }; map.addCallback('drawn', callback); this.div = document.createElement('div'); this.div.style.position = 'absolute'; this.div.style.width = this.dimensions.x + 'px'; this.div.style.height = this.dimensions.y + 'px'; //this.div.style.backgroundColor = 'white'; //this.div.style.border = 'solid black 1px'; this.div.innerHTML = content; MM.addEvent(this.div, 'mousedown', function(e) { if(!e) e = window.event; return MM.cancelEvent(e); }); map.parent.appendChild(this.div); this.draw(map); } MM.Follower.prototype = { div: null, coord: null, offset: null, dimensions: null, padding: null, draw: function(map) { try { var point = map.coordinatePoint(this.coord); } catch(e) { // too soon? return; } if(point.x + this.dimensions.x + this.offset.x < 0) { // too far left this.div.style.display = 'none'; } else if(point.y + this.dimensions.y + this.offset.y < 0) { // too far up this.div.style.display = 'none'; } else if(point.x + this.offset.x > map.dimensions.x) { // too far right this.div.style.display = 'none'; } else if(point.y + this.offset.y > map.dimensions.y) { // too far down this.div.style.display = 'none'; } else { this.div.style.display = 'block'; MM.moveElement(this.div, { x: Math.round(point.x + this.offset.x), y: Math.round(point.y + this.offset.y), scale: 1, width: this.dimensions.x, height: this.dimensions.y }); } } }; })(com.modestmaps); stamen-modestmaps-js-52d7710/examples/touch/0000775000175000017500000000000011653546654017761 5ustar daviddavidstamen-modestmaps-js-52d7710/examples/touch/index.html0000664000175000017500000000354111653546654021761 0ustar daviddavid Modest Maps JS - Touch Tester
stamen-modestmaps-js-52d7710/package.json0000664000175000017500000000131511653546654017307 0ustar daviddavid{ "name": "modestmaps", "description": "a display and interaction library for tile-based maps", "version": "0.21.0", "author": { "name": "Tom Carden", "email": "tom@tom-carden.co.uk", "url": "http://www.tom-carden.co.uk/" }, "contributors": [ "Mike Migurski ", "Tom MacWright " ], "keywords": ["map", "geo", "browser"], "main": "./modestmaps.js", "homepage": "https://github.com/stamen/modestmaps-js", "repositories": [{ "type" : "git", "url" : "git://github.com/stamen/modestmaps-js.git" }], "devDependencies": { "docco": "~0.3.0", "expresso": "~0.7.0", "uglify-js": "~1.0.0" }, "directories": { } } stamen-modestmaps-js-52d7710/modestmaps.min.js0000664000175000017500000007225711653546654020332 0ustar daviddavid/* * Modest Maps JS v0.21.0 * http://modestmaps.com/ * * Copyright (c) 2011 Stamen Design, All Rights Reserved. * * Open source under the BSD License. * http://creativecommons.org/licenses/BSD/ * * Versioned using Semantic Versioning (v.major.minor.patch) * See CHANGELOG and http://semver.org/ for more details. * */ if(!com){var com={};if(!com.modestmaps){com.modestmaps={}}}(function(a){a.extend=function(d,b){for(var c in b.prototype){if(typeof d.prototype[c]=="undefined"){d.prototype[c]=b.prototype[c]}}return d};a.getFrame=function(){return function(b){(window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(c){window.setTimeout(function(){c(+new Date())},10)})(b)}}();a.transformProperty=(function(d){if(!this.document){return}var c=document.documentElement.style;for(var b=0;b=c){return null}var f=d.column-b.column;var e=g.column%f;while(e<0){e+=f}return new a.Coordinate(g.row,e,g.zoom)}};a.TemplatedMapProvider=function(c,b){a.MapProvider.call(this,function(f){f=this.sourceCoordinate(f);if(!f){return null}var d=c;if(b&&b.length&&d.indexOf("{S}")>=0){var e=parseInt(f.zoom+f.row+f.column,10)%b.length;d=d.replace("{S}",b[e])}return d.replace("{Z}",f.zoom.toFixed(0)).replace("{X}",f.column.toFixed(0)).replace("{Y}",f.row.toFixed(0))})};a.extend(a.TemplatedMapProvider,a.MapProvider);a.getMousePoint=function(f,d){var b=new a.Point(f.clientX,f.clientY);b.x+=document.body.scrollLeft+document.documentElement.scrollLeft;b.y+=document.body.scrollTop+document.documentElement.scrollTop;for(var c=d.parent;c;c=c.offsetParent){b.x-=c.offsetLeft;b.y-=c.offsetTop}return b};a.MouseWheelHandler=function(b){if(b!==undefined){this.init(b)}};a.MouseWheelHandler.prototype={init:function(b){this.map=b;this._mouseWheel=a.bind(this.mouseWheel,this);a.addEvent(b.parent,"mousewheel",this._mouseWheel)},remove:function(){a.removeEvent(this.map.parent,"mousewheel",this._mouseWheel)},mouseWheel:function(d){var f=0;this.prevTime=this.prevTime||new Date().getTime();if(d.wheelDelta){f=d.wheelDelta}else{if(d.detail){f=-d.detail}}var c=new Date().getTime()-this.prevTime;if(Math.abs(f)>0&&(c>200)){var b=a.getMousePoint(d,this.map);this.map.zoomByAbout(f>0?1:-1,b);this.prevTime=new Date().getTime()}return a.cancelEvent(d)}};a.DoubleClickHandler=function(b){if(b!==undefined){this.init(b)}};a.DoubleClickHandler.prototype={init:function(b){this.map=b;this._doubleClick=a.bind(this.doubleClick,this);a.addEvent(b.parent,"dblclick",this._doubleClick)},remove:function(){a.removeEvent(this.map.parent,"dblclick",this._doubleClick)},doubleClick:function(c){var b=a.getMousePoint(c,this.map);this.map.zoomByAbout(c.shiftKey?-1:1,b);return a.cancelEvent(c)}};a.DragHandler=function(b){if(b!==undefined){this.init(b)}};a.DragHandler.prototype={init:function(b){this.map=b;this._mouseDown=a.bind(this.mouseDown,this);a.addEvent(b.parent,"mousedown",this._mouseDown)},remove:function(){a.removeEvent(this.map.parent,"mousedown",this._mouseDown)},mouseDown:function(b){a.addEvent(document,"mouseup",this._mouseUp=a.bind(this.mouseUp,this));a.addEvent(document,"mousemove",this._mouseMove=a.bind(this.mouseMove,this));this.prevMouse=new a.Point(b.clientX,b.clientY);this.map.parent.style.cursor="move";return a.cancelEvent(b)},mouseMove:function(b){if(this.prevMouse){this.map.panBy(b.clientX-this.prevMouse.x,b.clientY-this.prevMouse.y);this.prevMouse.x=b.clientX;this.prevMouse.y=b.clientY;this.prevMouse.t=+new Date()}return a.cancelEvent(b)},mouseUp:function(b){a.removeEvent(document,"mouseup",this._mouseUp);a.removeEvent(document,"mousemove",this._mouseMove);this.prevMouse=null;this.map.parent.style.cursor="";return a.cancelEvent(b)}};a.MouseHandler=function(b){if(b!==undefined){this.init(b)}};a.MouseHandler.prototype={init:function(b){this.map=b;this.handlers=[new a.DragHandler(b),new a.DoubleClickHandler(b),new a.MouseWheelHandler(b)]},remove:function(){for(var b=0;bthis.maxTapDistance){}else{if(f>this.maxTapTime){n.end=c;n.duration=f;this.onHold(n)}else{n.time=c;this.onTap(n)}}}var m={};for(var g=0;g=0;d--){var c=b[d];if(!(c.id in f)){this.loadingBay.removeChild(c);this.openRequestCount--;c.src=c.coord=c.onload=c.onerror=null}}for(var k in this.requestsById){if(this.requestsById.hasOwnProperty(k)){if(!(k in f)){var h=this.requestsById[k];delete this.requestsById[k];if(h!==null){h=h.id=h.coord=h.url=null}}}}},hasRequest:function(b){return(b in this.requestsById)},requestTile:function(e,d,b){if(!(e in this.requestsById)){var c={id:e,coord:d.copy(),url:b};this.requestsById[e]=c;if(b){this.requestQueue.push(c)}}},getProcessQueue:function(){if(!this._processQueue){var b=this;this._processQueue=function(){b.processQueue()}}return this._processQueue},processQueue:function(d){if(d&&this.requestQueue.length>8){this.requestQueue.sort(d)}while(this.openRequestCount0){var c=this.requestQueue.pop();if(c){this.openRequestCount++;var b=document.createElement("img");b.id=c.id;b.style.position="absolute";b.coord=c.coord;this.loadingBay.appendChild(b);b.onload=b.onerror=this.getLoadComplete();b.src=c.url;c=c.id=c.coord=c.url=null}}},_loadComplete:null,getLoadComplete:function(){if(!this._loadComplete){var b=this;this._loadComplete=function(d){d=d||window.event;var c=d.srcElement||d.target;c.onload=c.onerror=null;b.loadingBay.removeChild(c);b.openRequestCount--;delete b.requestsById[c.id];if(d.type==="load"&&(c.complete||(c.readyState&&c.readyState=="complete"))){b.dispatchCallback("requestcomplete",c)}else{c.src=null}setTimeout(b.getProcessQueue(),0)}}return this._loadComplete}};a.Map=function(e,h,f,g){if(typeof e=="string"){e=document.getElementById(e);if(!e){throw"The ID provided to modest maps could not be found."}}this.parent=e;this.parent.style.padding="0";this.parent.style.overflow="hidden";var b=a.getStyle(this.parent,"position");if(b!="relative"&&b!="absolute"){this.parent.style.position="relative"}if(!f){f=new a.Point(this.parent.offsetWidth,this.parent.offsetHeight);this.autoSize=true;var c=this;a.addEvent(window,"resize",this.windowResize())}else{this.autoSize=false;this.parent.style.width=Math.round(f.x)+"px";this.parent.style.height=Math.round(f.y)+"px"}this.dimensions=f;this.requestManager=new a.RequestManager(this.parent);this.requestManager.addCallback("requestcomplete",this.getTileComplete());this.layers={};this.layerParent=document.createElement("div");this.layerParent.id=this.parent.id+"-layers";this.layerParent.style.cssText="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0";this.parent.appendChild(this.layerParent);this.coordinate=new a.Coordinate(0.5,0.5,0);this.setProvider(h);this.enablePyramidLoading=false;this.callbackManager=new a.CallbackManager(this,["zoomed","panned","centered","extentset","resized","drawn"]);if(g===undefined){this.eventHandlers=[];this.eventHandlers.push(new a.MouseHandler(this))}else{this.eventHandlers=g;if(g instanceof Array){for(var d=0;db){e=e.zoomTo(b)}}}return e},draw:function(){this.coordinate=this.enforceLimits(this.coordinate);var p=Math.round(this.coordinate.zoom);if(this.dimensions.x<=0||this.dimensions.y<=0){if(this.autoSize){var r=this.parent.offsetWidth,C=this.parent.offsetHeight;this.dimensions=new a.Point(r,C);if(r<=0||C<=0){return}}else{return}}var x=this.pointCoordinate(new a.Point(0,0)).zoomTo(p).container();var u=this.pointCoordinate(this.dimensions).zoomTo(p).container().right().down();var l=0;if(l){x=x.left(l).up(l);u=u.right(l).down(l)}var I={};var m=this.createOrGetLayer(x.zoom);var e=x.copy();for(e.column=x.column;e.column<=u.column;e.column+=1){for(e.row=x.row;e.row<=u.row;e.row+=1){var n=e.toKey();I[n]=true;if(n in this.tiles){var L=this.tiles[n];if(L.parentNode!=m){m.appendChild(L)}}else{if(!this.requestManager.hasRequest(n)){var o=this.provider.getTileUrl(e);this.requestManager.requestTile(n,e,o)}var s=false;var H=e.zoom;for(var t=1;t<=H;t++){var v=e.zoomBy(-t).container();var B=v.toKey();if(this.enablePyramidLoading){I[B]=true;var D=this.createOrGetLayer(v.zoom);if(B in this.tiles){var y=this.tiles[B];if(y.parentNode!=D){D.appendChild(y)}}else{if(!this.requestManager.hasRequest(B)){this.requestManager.requestTile(B,v,this.provider.getTileUrl(v))}}}else{if(B in this.tiles){I[B]=true;s=true;break}}}if(!s&&!this.enablePyramidLoading){var k=e.zoomBy(1);I[k.toKey()]=true;k.column+=1;I[k.toKey()]=true;k.row+=1;I[k.toKey()]=true;k.column-=1;I[k.toKey()]=true}}}}for(var M in this.layers){if(this.layers.hasOwnProperty(M)){var d=parseInt(M,10);if(d>=x.zoom-5&&d=0;z--){K.removeChild(G[z])}}}var f=new Date().getTime();var E=x.zoom-5;var g=x.zoom+2;for(var A=E;A0){K.style.display="block";J=Math.pow(2,this.coordinate.zoom-A);c=c.zoomTo(A)}else{K.style.display="none"}var b=this.provider.tileWidth*J,q=this.provider.tileHeight*J,F=new a.Point(this.dimensions.x/2,this.dimensions.y/2);for(var z=G.length-1;z>=0;z--){var L=G[z];if(!I[L.id]){K.removeChild(L)}else{a.moveElement(L,{x:Math.round(F.x+(L.coord.column-c.column)*b),y:Math.round(F.y+(L.coord.row-c.row)*q),scale:J,width:this.provider.tileWidth,height:this.provider.tileHeight});this.recentTilesById[L.id].lastTouchedTime=f}}}this.requestManager.clearExcept(I);this.requestManager.processQueue(this.getCenterDistanceCompare());this.checkCache();this.dispatchCallback("drawn")},_tileComplete:null,getTileComplete:function(){if(!this._tileComplete){var b=this;this._tileComplete=function(g,h){b.tiles[h.id]=h;b.tileCacheSize++;var e={id:h.id,lastTouchedTime:new Date().getTime()};b.recentTilesById[h.id]=e;b.recentTiles.push(e);var f=b.coordinate.zoomTo(h.coord.zoom);var j=Math.pow(2,b.coordinate.zoom-h.coord.zoom);var d=((b.dimensions.x/2)+(h.coord.column-f.column)*b.provider.tileWidth*j);var c=((b.dimensions.y/2)+(h.coord.row-f.row)*b.provider.tileHeight*j);a.moveElement(h,{x:Math.round(d),y:Math.round(c),scale:j,width:b.provider.tileWidth,height:b.provider.tileHeight});var i=b.layers[h.coord.zoom];i.appendChild(h);h.className="map-tile-loaded";if(Math.round(b.coordinate.zoom)===h.coord.zoom){i.style.display="block"}b.requestRedraw()}}return this._tileComplete},_redrawTimer:undefined,requestRedraw:function(){if(!this._redrawTimer){this._redrawTimer=setTimeout(this.getRedraw(),1000)}},_redraw:null,getRedraw:function(){if(!this._redraw){var b=this;this._redraw=function(){b.draw();b._redrawTimer=0}}return this._redraw},createOrGetLayer:function(c){if(c in this.layers){return this.layers[c]}var b=document.createElement("div");b.id=this.parent.id+"-zoom-"+c;b.style.cssText=this.layerParent.style.cssText;b.style.zIndex=c;this.layerParent.appendChild(b);this.layers[c]=b;return b},checkCache:function(){var f=this.parent.getElementsByTagName("img").length;var d=Math.max(f,this.maxTileCacheSize);if(this.tileCacheSize>d){this.recentTiles.sort(function(h,g){return g.lastTouchedTimeh.lastTouchedTime?1:0})}while(this.tileCacheSize>d){var c=this.recentTiles.pop();var b=new Date().getTime();delete this.recentTilesById[c.id];var e=this.tiles[c.id];if(e.parentNode){alert("Gah: trying to removing cached tile even though it's still in the DOM")}else{delete this.tiles[c.id];this.tileCacheSize--}}},getCenterDistanceCompare:function(){var b=this.coordinate.zoomTo(Math.round(this.coordinate.zoom));return function(e,d){if(e&&d){var g=e.coord;var f=d.coord;if(g.zoom==f.zoom){var c=Math.abs(b.row-g.row-0.5)+Math.abs(b.column-g.column-0.5);var h=Math.abs(b.row-f.row-0.5)+Math.abs(b.column-f.column-0.5);return ch?-1:0}else{return g.zoomf.zoom?-1:0}}return e?1:d?-1:0}},destroy:function(){this.requestManager.clear();for(var b=0;b