├── .gitignore ├── README.md ├── examples ├── abq.html ├── auto-display-fields.html ├── cloudmade-styled-map.html ├── common │ ├── handlebars.js │ └── tabletop.js ├── google-custom-color.html ├── google-function-for-popup.html ├── google-with-callback-and-jquery.html ├── google-with-click-callback-and-map-options.html ├── google-with-info-callback.html ├── google-with-marker-customization.html ├── google.html ├── leaflet-with-everything.html ├── leaflet-with-marker-customization.html ├── leaflet.html ├── mapbox.html ├── mapquest-with-click.html └── mapquest.html └── src └── mapsheet.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Mapsheet.js** 2 | 3 | **Mapsheet.js** makes an interactive map out of a Google Spreadsheet. It depends on [Tabletop.js](http://github.com/jsoma/tabletop) and whatever map provider you're using (supports [Leaflet](http://leafletjs.com), [Google Maps](https://maps.google.com), [Mapbox](https://www.mapbox.com) (which is basically Leaflet!), and [MapQuest](http://developer.mapquest.com)). 4 | 5 | It is also **dead simple**. 6 | 7 | *Optional but fun:* Mapsheet supports [Handlebars](http://handlebarsjs.com) for info window templates. 8 | 9 | ## So how do I do this? 10 | 11 | First, you make a published Google Spreadsheet. You can find out how to do this in the [Tabletop docs](http://github.com/jsoma/tabletop). The only columns you actually need are *latitude* and *longitude*. We're also friendly enough to allow *lat*, *lng*, *long*, any probably anything else you can think of. 12 | 13 | A pretty full-fledged example might be: let's say you have some restaurants you'd like to map, and you want some info to show up in a popup when you click on the marker. Here goes nothing: 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 37 | 38 | 40 | 45 | 46 | 47 | 64 | 65 | 66 | ``` 67 | 68 | Most of the code is just including libraries! And if you'd like to map pet stores or murder scenes instead? Just make a different spreadsheet, change the URL, and edit the popup template. Voila: a brand new map. 69 | 70 | # Details 71 | 72 | ## The Moving Parts 73 | 74 | ### Mapsheet itself 75 | 76 | #### Initialization options 77 | 78 | When you initialize Mapsheet you have plenty of options to pick through. `key` and `element` are the only two required parameters, though! I've tried to note the ones that are provider-specific. 79 | 80 | `key` is the URL to the *published* Google Spreadsheet you're dealing with 81 | 82 | `element` is the id of the element that's going to become the map. You can also pass in an element. 83 | 84 | `click` is a callback for when a *marker* is clicked. **this** is the default **this**, the first parameter is the event and the second is the Mapsheet.Point (check the examples!) 85 | 86 | `fields` is an array of columns to display in the info window if you don't feel like using a template. Check the examples! 87 | 88 | `map` is the map, if you feel like rendering it without using a Mapsheet provider. 89 | 90 | `popupContent` is a function that returns content for the info window popup. It's passed the Tabletop model for the given row. It overrides both `fields` and `popupTemplate`. 91 | 92 | `popupTemplate` is the id of a [Handlebars](http://handlebarsjs.com) template that will be used for the info window that pops up when you click the marker on the map. You can also provider the compiled template instead. 93 | 94 | `provider` is the provider of the map service. I detail the providers a bit more below, but your options are 95 | 96 | Mapsheet.Providers.Leaflet 97 | Mapsheet.Providers.Google 98 | Mapsheet.Providers.MapBox 99 | Mapsheet.Providers.MapQuest 100 | 101 | `markerOptions` are passed through to the marker, and are used for things like specifying marker shadows and other very-specific-to-the-provider details. 102 | 103 | `titleColumn` is the column that you'd like to use as the title of the point (e.g., the name of the place). This is only important if using `fields`, or if you want hover effects for MapQuest or Google. 104 | 105 | `callback` is the callback for when the map has been successfully drawn. It will be passed two objects: the mapsheet object and the tabletop object. See [the Tabletop.js](http://github.com/jsoma/tabletop) docs if you'd like more info on that part. 106 | 107 | `callbackContext` provides the context of the callback, if necessary. 108 | 109 | `proxy` passes right through to Tabletop. 110 | 111 | `markerLayer` is the layer that you'd like to render your markers on **(not supported by Google)** 112 | 113 | `layerOptions` are options passed passed to the marker layer if you want a custom backing map, or specify attribution, etc **(Leaflet only)** 114 | 115 | #### Methods/Properties 116 | 117 | `.map()` is the rendered map 118 | 119 | `.points` is an array of Mapsheet.Point objects 120 | 121 | ### Mapsheet.Point 122 | 123 | #### Methods/Properties 124 | 125 | `.model` is the Tabletop model of the row the marker is associated with 126 | 127 | `.longitude()` is the longitude, pulled from a column named `lng`, `long` or `longitude` 128 | 129 | `.latitude()` is the latitude, pulled from a column named `lat` or `latitude` 130 | 131 | `.title()` gets the title of the marker - this column is set through `titleColumn` 132 | 133 | `.coords()` gives you the marker's `[ lat, lng ]` 134 | 135 | `.get(name)` returns the value of the given column. Not that it matters to you, but: Multi-word and capitalized things, like 'Web URL' are fixed up in an attempt to successfully read from tabletop, i.e. converted into lowercase with spaces removed (`Web URL` turns into `weburl`). 136 | 137 | `.content()` uses `popupContent`, `popupTemplate`, or `titleColumn` and `fields` to create content for the info window popup 138 | 139 | `.isValid()` returns true if latitude() and longitude() both return valid floats 140 | 141 | `.marker` returns the raw marker (from Google or MapQuest etc) 142 | 143 | ### Mapsheet.Provider.Whatever 144 | 145 | Must respond to `.initialize(id)` and `.drawPoints(pointsArray)`. 146 | 147 | Their constructor sets up the options, `initialize` draws the map, and `drawPoints` puts the points on it and auto-fits the zoom and bounds. 148 | 149 | ## Providers 150 | 151 | [Google Maps](https://developers.google.com/maps/) as `Mapsheet.Providers.Google` (default) 152 | 153 | [Leaflet](http://leafletjs.com) (using MapQuest tiles by default, but supports CloudMade, etc) as `Mapsheet.Providers.Leaflet` (be sure to include the CSS) 154 | 155 | [MapBox](http://mapbox.com) as `Mapsheet.Providers.MapBox` 156 | 157 | [MapQuest](http://developer.mapquest.com/web/products/open) as `Mapsheet.Providers.MapQuest` 158 | 159 | # Customizing Your Map 160 | 161 | ## Styling info windows 162 | 163 | All of the info boxes live inside of a `
`, so feel free to apply styles to `.mapsheet-popup h3`, etc 164 | 165 | ## Customizing the map itself (roads, water, etc) 166 | 167 | You'll need to pick a provider that allows map customization 168 | 169 | [CloudMade](http://www.cloudmade.com) has a billion styles over at their [map style editor](http://maps.cloudmade.com/editor), you'll just have to check out the `cloudmade.html` example. You need an API key, but it ain't tough beyond that. 170 | 171 | [MapBox](http://www.mapbox.com) could also work for you, especially if you have a lot of stuff going on. 172 | 173 | ## Custom Markers 174 | 175 | I think it's *really stupid* that, generally speaking, providers make it so tough to have custom-colored markers. So I tried to make it easy! 176 | 177 | Custom markers are supported for Google Maps, Leaflet, and MapBox (to varying degrees). 178 | 179 | ### Possibilities 180 | 181 | **Custom color:** Add a `hex color` column in your spreadsheet to customize the color of each marker. 182 | 183 | **Custom icon:** If you want to an specify icon for each point, add an `icon url` column in your spreadsheet. You'll probably want to make it the full path, i.e. `http://blahblah.com/images/icons/blah.png` 184 | 185 | **Custom default icon:** If want every point to have the same marker, just not the default, you can pass a default icon URL through `markerOptions`, typically as `iconUrl` 186 | 187 | **Custom icon + shadows:** Check the `examples/leaflet-with-marker-customization.html` example, it's complicated. 188 | 189 | ### Provider support 190 | 191 | **Google Maps:** custom colors, custom icons, custom default icon 192 | 193 | **MapBox:** custom icons 194 | 195 | **Leaflet:** custom icons, custom icons + shadows, custom default icon 196 | 197 | ### General notes on using colored/custom icons 198 | 199 | If you're unsure of what hex colors are, pop on over to [http://www.colorpicker.com](http://www.colorpicker.com) or [Wikipedia](http://en.wikipedia.org/wiki/Web_colors#X11_color_names) - they're just strings of numbers/letters like FF36A2 that end up as colors.* 200 | 201 | When you customize your icons using Leaflet, you'll probably want to pass some `markerOptions` as well to make sure everything lines up nicely. `markerOptions: { iconSize: [32, 37], iconAnchor: [16, 37] }` works for the icons from the [Map Icons Collection](http://mapicons.nicolasmollet.com). [Read more on the Leaflet site](http://leafletjs.com/examples/custom-icons.html). 202 | 203 | ### Using other Leaflet tile providers 204 | 205 | Leaflet defaults to serving tiles from MapQuest, because it's free, and doesn't require an API key. If you'd like to use [CloudMade](http://www.cloudmade.com) or something instead, you'll need to make some changes. You'll need to pass in a mapOptions along the lines of the following 206 | 207 | mapOptions: { 208 | subdomains: 'abc', 209 | tilePath: 'http://{s}.tile.cloudmade.com/{apikey}/{styleId}/256/{z}/{x}/{y}.png', 210 | styleId: 1930, 211 | apikey: 'Your CloudMade API Key', 212 | attribution: 'An appropriate credit line without MapQuest' 213 | } 214 | 215 | And then you should be all set! 216 | 217 | # Examples 218 | 219 | There are a lot of examples in `/examples`, check them out! 220 | 221 | If you're looking for the least complicated one, check out `fields.html`. It isn't too customizable but it gets the job done. `leaflet-with-marker-customization.html` is the zaniest of the bunch. 222 | 223 | # Etc 224 | 225 | ## Credits 226 | 227 | Hi, I'm [Jonathan Soma](http://twitter.com/dangerscarf). I run the [Brooklyn Brainery](http://brooklynbrainery.com), and make all sorts of nonsense. 228 | 229 | ## Todo 230 | 231 | Fix up the docs to actually be nice and pleasant 232 | 233 | Don't show popups if there's no content in the popup 234 | 235 | Replace the sample map with some quality, dedicated sample maps 236 | 237 | Callbacks callbacks everywhere 238 | 239 | Notes about styling your infoboxes -------------------------------------------------------------------------------- /examples/abq.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | Interactive Map: 'Bad' on location 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 49 | 50 | 64 | 65 |
66 |
67 | X 68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 |
78 | 79 | 87 | 88 | 89 | 90 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /examples/auto-display-fields.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/cloudmade-styled-map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/common/handlebars.js: -------------------------------------------------------------------------------- 1 | // lib/handlebars/base.js 2 | var Handlebars = {}; 3 | 4 | Handlebars.VERSION = "1.0.beta.6"; 5 | 6 | Handlebars.helpers = {}; 7 | Handlebars.partials = {}; 8 | 9 | Handlebars.registerHelper = function(name, fn, inverse) { 10 | if(inverse) { fn.not = inverse; } 11 | this.helpers[name] = fn; 12 | }; 13 | 14 | Handlebars.registerPartial = function(name, str) { 15 | this.partials[name] = str; 16 | }; 17 | 18 | Handlebars.registerHelper('helperMissing', function(arg) { 19 | if(arguments.length === 2) { 20 | return undefined; 21 | } else { 22 | throw new Error("Could not find property '" + arg + "'"); 23 | } 24 | }); 25 | 26 | var toString = Object.prototype.toString, functionType = "[object Function]"; 27 | 28 | Handlebars.registerHelper('blockHelperMissing', function(context, options) { 29 | var inverse = options.inverse || function() {}, fn = options.fn; 30 | 31 | 32 | var ret = ""; 33 | var type = toString.call(context); 34 | 35 | if(type === functionType) { context = context.call(this); } 36 | 37 | if(context === true) { 38 | return fn(this); 39 | } else if(context === false || context == null) { 40 | return inverse(this); 41 | } else if(type === "[object Array]") { 42 | if(context.length > 0) { 43 | for(var i=0, j=context.length; i 0) { 60 | for(var i=0, j=context.length; i 2) { 235 | expected.push("'" + this.terminals_[p] + "'"); 236 | } 237 | var errStr = ""; 238 | if (this.lexer.showPosition) { 239 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + this.terminals_[symbol] + "'"; 240 | } else { 241 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 242 | } 243 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 244 | } 245 | } 246 | if (action[0] instanceof Array && action.length > 1) { 247 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 248 | } 249 | switch (action[0]) { 250 | case 1: 251 | stack.push(symbol); 252 | vstack.push(this.lexer.yytext); 253 | lstack.push(this.lexer.yylloc); 254 | stack.push(action[1]); 255 | symbol = null; 256 | if (!preErrorSymbol) { 257 | yyleng = this.lexer.yyleng; 258 | yytext = this.lexer.yytext; 259 | yylineno = this.lexer.yylineno; 260 | yyloc = this.lexer.yylloc; 261 | if (recovering > 0) 262 | recovering--; 263 | } else { 264 | symbol = preErrorSymbol; 265 | preErrorSymbol = null; 266 | } 267 | break; 268 | case 2: 269 | len = this.productions_[action[1]][1]; 270 | yyval.$ = vstack[vstack.length - len]; 271 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 272 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 273 | if (typeof r !== "undefined") { 274 | return r; 275 | } 276 | if (len) { 277 | stack = stack.slice(0, -1 * len * 2); 278 | vstack = vstack.slice(0, -1 * len); 279 | lstack = lstack.slice(0, -1 * len); 280 | } 281 | stack.push(this.productions_[action[1]][0]); 282 | vstack.push(yyval.$); 283 | lstack.push(yyval._$); 284 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 285 | stack.push(newState); 286 | break; 287 | case 3: 288 | return true; 289 | } 290 | } 291 | return true; 292 | } 293 | };/* Jison generated lexer */ 294 | var lexer = (function(){ 295 | 296 | var lexer = ({EOF:1, 297 | parseError:function parseError(str, hash) { 298 | if (this.yy.parseError) { 299 | this.yy.parseError(str, hash); 300 | } else { 301 | throw new Error(str); 302 | } 303 | }, 304 | setInput:function (input) { 305 | this._input = input; 306 | this._more = this._less = this.done = false; 307 | this.yylineno = this.yyleng = 0; 308 | this.yytext = this.matched = this.match = ''; 309 | this.conditionStack = ['INITIAL']; 310 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 311 | return this; 312 | }, 313 | input:function () { 314 | var ch = this._input[0]; 315 | this.yytext+=ch; 316 | this.yyleng++; 317 | this.match+=ch; 318 | this.matched+=ch; 319 | var lines = ch.match(/\n/); 320 | if (lines) this.yylineno++; 321 | this._input = this._input.slice(1); 322 | return ch; 323 | }, 324 | unput:function (ch) { 325 | this._input = ch + this._input; 326 | return this; 327 | }, 328 | more:function () { 329 | this._more = true; 330 | return this; 331 | }, 332 | pastInput:function () { 333 | var past = this.matched.substr(0, this.matched.length - this.match.length); 334 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 335 | }, 336 | upcomingInput:function () { 337 | var next = this.match; 338 | if (next.length < 20) { 339 | next += this._input.substr(0, 20-next.length); 340 | } 341 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 342 | }, 343 | showPosition:function () { 344 | var pre = this.pastInput(); 345 | var c = new Array(pre.length + 1).join("-"); 346 | return pre + this.upcomingInput() + "\n" + c+"^"; 347 | }, 348 | next:function () { 349 | if (this.done) { 350 | return this.EOF; 351 | } 352 | if (!this._input) this.done = true; 353 | 354 | var token, 355 | match, 356 | col, 357 | lines; 358 | if (!this._more) { 359 | this.yytext = ''; 360 | this.match = ''; 361 | } 362 | var rules = this._currentRules(); 363 | for (var i=0;i < rules.length; i++) { 364 | match = this._input.match(this.rules[rules[i]]); 365 | if (match) { 366 | lines = match[0].match(/\n.*/g); 367 | if (lines) this.yylineno += lines.length; 368 | this.yylloc = {first_line: this.yylloc.last_line, 369 | last_line: this.yylineno+1, 370 | first_column: this.yylloc.last_column, 371 | last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length} 372 | this.yytext += match[0]; 373 | this.match += match[0]; 374 | this.matches = match; 375 | this.yyleng = this.yytext.length; 376 | this._more = false; 377 | this._input = this._input.slice(match[0].length); 378 | this.matched += match[0]; 379 | token = this.performAction.call(this, this.yy, this, rules[i],this.conditionStack[this.conditionStack.length-1]); 380 | if (token) return token; 381 | else return; 382 | } 383 | } 384 | if (this._input === "") { 385 | return this.EOF; 386 | } else { 387 | this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 388 | {text: "", token: null, line: this.yylineno}); 389 | } 390 | }, 391 | lex:function lex() { 392 | var r = this.next(); 393 | if (typeof r !== 'undefined') { 394 | return r; 395 | } else { 396 | return this.lex(); 397 | } 398 | }, 399 | begin:function begin(condition) { 400 | this.conditionStack.push(condition); 401 | }, 402 | popState:function popState() { 403 | return this.conditionStack.pop(); 404 | }, 405 | _currentRules:function _currentRules() { 406 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 407 | }, 408 | topState:function () { 409 | return this.conditionStack[this.conditionStack.length-2]; 410 | }, 411 | pushState:function begin(condition) { 412 | this.begin(condition); 413 | }}); 414 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 415 | 416 | var YYSTATE=YY_START 417 | switch($avoiding_name_collisions) { 418 | case 0: 419 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 420 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 421 | if(yy_.yytext) return 14; 422 | 423 | break; 424 | case 1: return 14; 425 | break; 426 | case 2: this.popState(); return 14; 427 | break; 428 | case 3: return 24; 429 | break; 430 | case 4: return 16; 431 | break; 432 | case 5: return 20; 433 | break; 434 | case 6: return 19; 435 | break; 436 | case 7: return 19; 437 | break; 438 | case 8: return 23; 439 | break; 440 | case 9: return 23; 441 | break; 442 | case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 443 | break; 444 | case 11: return 22; 445 | break; 446 | case 12: return 34; 447 | break; 448 | case 13: return 33; 449 | break; 450 | case 14: return 33; 451 | break; 452 | case 15: return 36; 453 | break; 454 | case 16: /*ignore whitespace*/ 455 | break; 456 | case 17: this.popState(); return 18; 457 | break; 458 | case 18: this.popState(); return 18; 459 | break; 460 | case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 28; 461 | break; 462 | case 20: return 30; 463 | break; 464 | case 21: return 30; 465 | break; 466 | case 22: return 29; 467 | break; 468 | case 23: return 33; 469 | break; 470 | case 24: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 33; 471 | break; 472 | case 25: return 'INVALID'; 473 | break; 474 | case 26: return 5; 475 | break; 476 | } 477 | }; 478 | lexer.rules = [/^[^\x00]*?(?=(\{\{))/,/^[^\x00]+/,/^[^\x00]{2,}?(?=(\{\{))/,/^\{\{>/,/^\{\{#/,/^\{\{\//,/^\{\{\^/,/^\{\{\s*else\b/,/^\{\{\{/,/^\{\{&/,/^\{\{![\s\S]*?\}\}/,/^\{\{/,/^=/,/^\.(?=[} ])/,/^\.\./,/^[\/.]/,/^\s+/,/^\}\}\}/,/^\}\}/,/^"(\\["]|[^"])*"/,/^true(?=[}\s])/,/^false(?=[}\s])/,/^[0-9]+(?=[}\s])/,/^[a-zA-Z0-9_$-]+(?=[=}\s\/.])/,/^\[[^\]]*\]/,/^./,/^$/]; 479 | lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,26],"inclusive":true}};return lexer;})() 480 | parser.lexer = lexer; 481 | return parser; 482 | })(); 483 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 484 | exports.parser = handlebars; 485 | exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } 486 | exports.main = function commonjsMain(args) { 487 | if (!args[1]) 488 | throw new Error('Usage: '+args[0]+' FILE'); 489 | if (typeof process !== 'undefined') { 490 | var source = require('fs').readFileSync(require('path').join(process.cwd(), args[1]), "utf8"); 491 | } else { 492 | var cwd = require("file").path(require("file").cwd()); 493 | var source = cwd.join(args[1]).read({charset: "utf-8"}); 494 | } 495 | return exports.parser.parse(source); 496 | } 497 | if (typeof module !== 'undefined' && require.main === module) { 498 | exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); 499 | } 500 | }; 501 | ; 502 | // lib/handlebars/compiler/base.js 503 | Handlebars.Parser = handlebars; 504 | 505 | Handlebars.parse = function(string) { 506 | Handlebars.Parser.yy = Handlebars.AST; 507 | return Handlebars.Parser.parse(string); 508 | }; 509 | 510 | Handlebars.print = function(ast) { 511 | return new Handlebars.PrintVisitor().accept(ast); 512 | }; 513 | 514 | Handlebars.logger = { 515 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 516 | 517 | // override in the host environment 518 | log: function(level, str) {} 519 | }; 520 | 521 | Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; 522 | ; 523 | // lib/handlebars/compiler/ast.js 524 | (function() { 525 | 526 | Handlebars.AST = {}; 527 | 528 | Handlebars.AST.ProgramNode = function(statements, inverse) { 529 | this.type = "program"; 530 | this.statements = statements; 531 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 532 | }; 533 | 534 | Handlebars.AST.MustacheNode = function(params, hash, unescaped) { 535 | this.type = "mustache"; 536 | this.id = params[0]; 537 | this.params = params.slice(1); 538 | this.hash = hash; 539 | this.escaped = !unescaped; 540 | }; 541 | 542 | Handlebars.AST.PartialNode = function(id, context) { 543 | this.type = "partial"; 544 | 545 | // TODO: disallow complex IDs 546 | 547 | this.id = id; 548 | this.context = context; 549 | }; 550 | 551 | var verifyMatch = function(open, close) { 552 | if(open.original !== close.original) { 553 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 554 | } 555 | }; 556 | 557 | Handlebars.AST.BlockNode = function(mustache, program, close) { 558 | verifyMatch(mustache.id, close); 559 | this.type = "block"; 560 | this.mustache = mustache; 561 | this.program = program; 562 | }; 563 | 564 | Handlebars.AST.InverseNode = function(mustache, program, close) { 565 | verifyMatch(mustache.id, close); 566 | this.type = "inverse"; 567 | this.mustache = mustache; 568 | this.program = program; 569 | }; 570 | 571 | Handlebars.AST.ContentNode = function(string) { 572 | this.type = "content"; 573 | this.string = string; 574 | }; 575 | 576 | Handlebars.AST.HashNode = function(pairs) { 577 | this.type = "hash"; 578 | this.pairs = pairs; 579 | }; 580 | 581 | Handlebars.AST.IdNode = function(parts) { 582 | this.type = "ID"; 583 | this.original = parts.join("."); 584 | 585 | var dig = [], depth = 0; 586 | 587 | for(var i=0,l=parts.length; i": ">", 646 | '"': """, 647 | "'": "'", 648 | "`": "`" 649 | }; 650 | 651 | var badChars = /&(?!\w+;)|[<>"'`]/g; 652 | var possible = /[&<>"'`]/; 653 | 654 | var escapeChar = function(chr) { 655 | return escape[chr] || "&"; 656 | }; 657 | 658 | Handlebars.Utils = { 659 | escapeExpression: function(string) { 660 | // don't escape SafeStrings, since they're already safe 661 | if (string instanceof Handlebars.SafeString) { 662 | return string.toString(); 663 | } else if (string == null || string === false) { 664 | return ""; 665 | } 666 | 667 | if(!possible.test(string)) { return string; } 668 | return string.replace(badChars, escapeChar); 669 | }, 670 | 671 | isEmpty: function(value) { 672 | if (typeof value === "undefined") { 673 | return true; 674 | } else if (value === null) { 675 | return true; 676 | } else if (value === false) { 677 | return true; 678 | } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { 679 | return true; 680 | } else { 681 | return false; 682 | } 683 | } 684 | }; 685 | })();; 686 | // lib/handlebars/compiler/compiler.js 687 | Handlebars.Compiler = function() {}; 688 | Handlebars.JavaScriptCompiler = function() {}; 689 | 690 | (function(Compiler, JavaScriptCompiler) { 691 | Compiler.OPCODE_MAP = { 692 | appendContent: 1, 693 | getContext: 2, 694 | lookupWithHelpers: 3, 695 | lookup: 4, 696 | append: 5, 697 | invokeMustache: 6, 698 | appendEscaped: 7, 699 | pushString: 8, 700 | truthyOrFallback: 9, 701 | functionOrFallback: 10, 702 | invokeProgram: 11, 703 | invokePartial: 12, 704 | push: 13, 705 | assignToHash: 15, 706 | pushStringParam: 16 707 | }; 708 | 709 | Compiler.MULTI_PARAM_OPCODES = { 710 | appendContent: 1, 711 | getContext: 1, 712 | lookupWithHelpers: 2, 713 | lookup: 1, 714 | invokeMustache: 3, 715 | pushString: 1, 716 | truthyOrFallback: 1, 717 | functionOrFallback: 1, 718 | invokeProgram: 3, 719 | invokePartial: 1, 720 | push: 1, 721 | assignToHash: 1, 722 | pushStringParam: 1 723 | }; 724 | 725 | Compiler.DISASSEMBLE_MAP = {}; 726 | 727 | for(var prop in Compiler.OPCODE_MAP) { 728 | var value = Compiler.OPCODE_MAP[prop]; 729 | Compiler.DISASSEMBLE_MAP[value] = prop; 730 | } 731 | 732 | Compiler.multiParamSize = function(code) { 733 | return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; 734 | }; 735 | 736 | Compiler.prototype = { 737 | compiler: Compiler, 738 | 739 | disassemble: function() { 740 | var opcodes = this.opcodes, opcode, nextCode; 741 | var out = [], str, name, value; 742 | 743 | for(var i=0, l=opcodes.length; i 0) { 1128 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1129 | } 1130 | 1131 | // Generate minimizer alias mappings 1132 | if (!this.isChild) { 1133 | var aliases = [] 1134 | for (var alias in this.context.aliases) { 1135 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1136 | } 1137 | } 1138 | 1139 | if (this.source[1]) { 1140 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1141 | } 1142 | 1143 | // Merge children 1144 | if (!this.isChild) { 1145 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1146 | } 1147 | 1148 | if (!this.environment.isSimple) { 1149 | this.source.push("return buffer;"); 1150 | } 1151 | 1152 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1153 | 1154 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1406 | return "stack" + this.stackSlot; 1407 | }, 1408 | 1409 | popStack: function() { 1410 | return "stack" + this.stackSlot--; 1411 | }, 1412 | 1413 | topStack: function() { 1414 | return "stack" + this.stackSlot; 1415 | }, 1416 | 1417 | quotedString: function(str) { 1418 | return '"' + str 1419 | .replace(/\\/g, '\\\\') 1420 | .replace(/"/g, '\\"') 1421 | .replace(/\n/g, '\\n') 1422 | .replace(/\r/g, '\\r') + '"'; 1423 | } 1424 | }; 1425 | 1426 | var reservedWords = ( 1427 | "break else new var" + 1428 | " case finally return void" + 1429 | " catch for switch while" + 1430 | " continue function this with" + 1431 | " default if throw" + 1432 | " delete in try" + 1433 | " do instanceof typeof" + 1434 | " abstract enum int short" + 1435 | " boolean export interface static" + 1436 | " byte extends long super" + 1437 | " char final native synchronized" + 1438 | " class float package throws" + 1439 | " const goto private transient" + 1440 | " debugger implements protected volatile" + 1441 | " double import public let yield" 1442 | ).split(" "); 1443 | 1444 | var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; 1445 | 1446 | for(var i=0, l=reservedWords.length; i 1 && this.debug) { 271 | this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); 272 | } 273 | return this.models[ this.model_names[0] ].all(); 274 | } else { 275 | return this.models; 276 | } 277 | }, 278 | 279 | /* 280 | Add another sheet to the wanted list 281 | */ 282 | addWanted: function(sheet) { 283 | if(ttIndexOf(this.wanted, sheet) === -1) { 284 | this.wanted.push(sheet); 285 | } 286 | }, 287 | 288 | /* 289 | Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 290 | Need to use injectScript because the worksheet view that you're working from 291 | doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 292 | Calls back to loadSheet in order to get the real work done. 293 | 294 | Used as a callback for the worksheet-based JSON 295 | */ 296 | loadSheets: function(data) { 297 | var i, ilen; 298 | var toLoad = []; 299 | this.foundSheetNames = []; 300 | 301 | for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 302 | this.foundSheetNames.push(data.feed.entry[i].title.$t); 303 | // Only pull in desired sheets to reduce loading 304 | if( this.isWanted(data.feed.entry[i].content.$t) ) { 305 | var sheet_id = data.feed.entry[i].link[3].href.substr( data.feed.entry[i].link[3].href.length - 3, 3); 306 | var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?sq=" + this.query + '&alt=' 307 | if (inNodeJS || supportsCORS) { 308 | json_path += 'json'; 309 | } else { 310 | json_path += 'json-in-script'; 311 | } 312 | if(this.orderby) { 313 | json_path += "&orderby=column:" + this.orderby.toLowerCase(); 314 | } 315 | if(this.reverse) { 316 | json_path += "&reverse=true"; 317 | } 318 | toLoad.push(json_path); 319 | } 320 | } 321 | 322 | this.sheetsToLoad = toLoad.length; 323 | for(i = 0, ilen = toLoad.length; i < ilen; i++) { 324 | this.requestData(toLoad[i], this.loadSheet); 325 | } 326 | }, 327 | 328 | /* 329 | Access layer for the this.models 330 | .sheets() gets you all of the sheets 331 | .sheets('Sheet1') gets you the sheet named Sheet1 332 | */ 333 | sheets: function(sheetName) { 334 | if(typeof sheetName === "undefined") { 335 | return this.models; 336 | } else { 337 | if(typeof(this.models[ sheetName ]) === "undefined") { 338 | // alert( "Can't find " + sheetName ); 339 | return; 340 | } else { 341 | return this.models[ sheetName ]; 342 | } 343 | } 344 | }, 345 | 346 | /* 347 | Parse a single list-based worksheet, turning it into a Tabletop Model 348 | 349 | Used as a callback for the list-based JSON 350 | */ 351 | loadSheet: function(data) { 352 | var model = new Tabletop.Model( { data: data, 353 | parseNumbers: this.parseNumbers, 354 | postProcess: this.postProcess, 355 | tabletop: this } ); 356 | this.models[ model.name ] = model; 357 | if(ttIndexOf(this.model_names, model.name) === -1) { 358 | this.model_names.push(model.name); 359 | } 360 | this.sheetsToLoad--; 361 | if(this.sheetsToLoad === 0) 362 | this.doCallback(); 363 | }, 364 | 365 | /* 366 | Execute the callback upon loading! Rely on this.data() because you might 367 | only request certain pieces of data (i.e. simpleSheet mode) 368 | Tests this.sheetsToLoad just in case a race condition happens to show up 369 | */ 370 | doCallback: function() { 371 | if(this.sheetsToLoad === 0) { 372 | this.callback.apply(this.callbackContext || this, [this.data(), this]); 373 | } 374 | }, 375 | 376 | log: function(msg) { 377 | if(this.debug) { 378 | if(typeof console !== "undefined" && typeof console.log !== "undefined") { 379 | Function.prototype.apply.apply(console.log, [console, arguments]); 380 | } 381 | } 382 | } 383 | 384 | }; 385 | 386 | /* 387 | Tabletop.Model stores the attribute names and parses the worksheet data 388 | to turn it into something worthwhile 389 | 390 | Options should be in the format { data: XXX }, with XXX being the list-based worksheet 391 | */ 392 | Tabletop.Model = function(options) { 393 | var i, j, ilen, jlen; 394 | this.column_names = []; 395 | this.name = options.data.feed.title.$t; 396 | this.elements = []; 397 | this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 398 | 399 | if(typeof(options.data.feed.entry) === 'undefined') { 400 | options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); 401 | this.elements = []; 402 | return; 403 | } 404 | 405 | for(var key in options.data.feed.entry[0]){ 406 | if(/^gsx/.test(key)) 407 | this.column_names.push( key.replace("gsx$","") ); 408 | } 409 | 410 | for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 411 | var source = options.data.feed.entry[i]; 412 | var element = {}; 413 | for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { 414 | var cell = source[ "gsx$" + this.column_names[j] ]; 415 | if (typeof(cell) !== 'undefined') { 416 | if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) 417 | element[ this.column_names[j] ] = +cell.$t; 418 | else 419 | element[ this.column_names[j] ] = cell.$t; 420 | } else { 421 | element[ this.column_names[j] ] = ''; 422 | } 423 | } 424 | if(element.rowNumber === undefined) 425 | element.rowNumber = i + 1; 426 | if( options.postProcess ) 427 | options.postProcess(element); 428 | this.elements.push(element); 429 | } 430 | 431 | }; 432 | 433 | Tabletop.Model.prototype = { 434 | /* 435 | Returns all of the elements (rows) of the worksheet as objects 436 | */ 437 | all: function() { 438 | return this.elements; 439 | }, 440 | 441 | /* 442 | Return the elements as an array of arrays, instead of an array of objects 443 | */ 444 | toArray: function() { 445 | var array = [], 446 | i, j, ilen, jlen; 447 | for(i = 0, ilen = this.elements.length; i < ilen; i++) { 448 | var row = []; 449 | for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 450 | row.push( this.elements[i][ this.column_names[j] ] ); 451 | } 452 | array.push(row); 453 | } 454 | return array; 455 | } 456 | }; 457 | 458 | if(inNodeJS) { 459 | module.exports = Tabletop; 460 | } else { 461 | global.Tabletop = Tabletop; 462 | } 463 | 464 | })(this); 465 | -------------------------------------------------------------------------------- /examples/google-custom-color.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/google-function-for-popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/google-with-callback-and-jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 32 | 33 |

The towns are:

34 |
    35 |
36 | 37 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/google-with-click-callback-and-map-options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/google-with-info-callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 |

The towns are:

26 | 27 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/google-with-marker-customization.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/google.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/leaflet-with-everything.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/leaflet-with-marker-customization.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/leaflet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 36 | 37 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/mapbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |

Because of how the mapbox CSS works, you can't see markers unless it's uploaded online

14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/mapquest-with-click.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/mapquest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/mapsheet.js: -------------------------------------------------------------------------------- 1 | (function(global) { 2 | "use strict"; 3 | 4 | function merge_options(obj1, obj2) { 5 | var obj3 = {}; 6 | var attrname; 7 | for (attrname in obj1) { obj3[attrname] = obj1[attrname]; } 8 | for (attrname in obj2) { obj3[attrname] = obj2[attrname]; } 9 | return obj3; 10 | } 11 | 12 | var Mapsheet = global.Mapsheet = function(options) { 13 | // Make sure Mapsheet is being used as a constructor no matter what. 14 | if(!this || !(this instanceof Mapsheet)) { 15 | return new Mapsheet(options); 16 | } 17 | 18 | this.key = options.key; 19 | this.click = options.click; 20 | this.passedMap = options.map; 21 | this.element = options.element; 22 | this.sheetName = options.sheetName; 23 | this.provider = options.provider || Mapsheet.Providers.Google; 24 | this.renderer = new this.provider( { map: options.passedMap, mapOptions: options.mapOptions, layerOptions: options.layerOptions, markerLayer: options.markerLayer } ); 25 | this.fields = options.fields; 26 | this.titleColumn = options.titleColumn; 27 | this.popupContent = options.popupContent; 28 | this.popupTemplate = options.popupTemplate; 29 | this.callbackContext = options.callbackContext; 30 | this.callback = options.callback; 31 | 32 | // Let's automatically engage simpleSheet mode, 33 | // which allows for easier using of data later on 34 | // if you have multiple sheets, you'll want to 35 | // disable this 36 | var simpleSheet = true; 37 | 38 | if(typeof(this.popupTemplate) === 'string') { 39 | var source = document.getElementById(this.popupTemplate).innerHTML; 40 | this.popupTemplate = Handlebars.compile(source); 41 | } 42 | this.markerOptions = options.markerOptions || {}; 43 | 44 | if(typeof(this.element) === 'string') { 45 | this.element = document.getElementById(this.element); 46 | }; 47 | 48 | this.tabletop = new Tabletop( { key: this.key, callback: this.loadPoints, callbackContext: this, simpleSheet: simpleSheet, proxy: options.proxy } ); 49 | }; 50 | 51 | 52 | Mapsheet.prototype = { 53 | 54 | loadPoints: function(data, tabletop) { 55 | this.points = []; 56 | 57 | if(typeof(this.sheetName) === 'undefined') { 58 | this.sheetName = tabletop.model_names[0]; 59 | } 60 | 61 | var elements = tabletop.sheets(this.sheetName).elements; 62 | 63 | for(var i = 0; i < elements.length; i++) { 64 | var point = new Mapsheet.Point( { model: elements[i], fields: this.fields, popupContent: this.popupContent, popupTemplate: this.popupTemplate, markerOptions: this.markerOptions, titleColumn: this.titleColumn, click: this.click } ); 65 | this.points.push(point); 66 | }; 67 | 68 | this.draw(); 69 | }, 70 | 71 | draw: function() { 72 | this.renderer.initialize(this.element); 73 | this.renderer.drawPoints(this.points); 74 | if(this.callback) { 75 | this.callback.apply(this.callbackContext || this, [this, this.tabletop]); 76 | } 77 | }, 78 | 79 | log: function(msg) { 80 | if(this.debug) { 81 | if(typeof console !== "undefined" && typeof console.log !== "undefined") { 82 | Function.prototype.apply.apply(console.log, [console, arguments]); 83 | } 84 | } 85 | }, 86 | 87 | map: function() { 88 | return (this.passedMap || this.renderer.map); 89 | } 90 | 91 | }; 92 | 93 | Mapsheet.Point = function(options) { 94 | this.model = options.model; 95 | this.fields = options.fields; 96 | this.popupContent = options.popupContent; 97 | this.popupTemplate = options.popupTemplate; 98 | this.titleColumn = options.titleColumn; 99 | this.markerOptions = options.markerOptions; 100 | this.click = options.click 101 | }; 102 | 103 | Mapsheet.Point.prototype = { 104 | coords: function() { 105 | return [ this.latitude(), this.longitude() ]; 106 | }, 107 | 108 | latitude: function() { 109 | return parseFloat( this.model["latitude"] || this.model["lat"] ); 110 | }, 111 | 112 | longitude: function() { 113 | return parseFloat( this.model["longitude"] || this.model["lng"] || this.model["long"] ); 114 | }, 115 | 116 | get: function(fieldName) { 117 | if(typeof(fieldName) === 'undefined') { 118 | return; 119 | } 120 | return this.model[fieldName.toLowerCase().replace(/ +/,'')]; 121 | }, 122 | 123 | title: function() { 124 | return this.get(this.titleColumn); 125 | }, 126 | 127 | isValid: function() { 128 | return !isNaN( this.latitude() ) && !isNaN( this.longitude() ) 129 | }, 130 | 131 | content: function() { 132 | var html = ""; 133 | if(typeof(this.popupContent) !== 'undefined') { 134 | html = this.popupContent.call(this, this.model); 135 | } else if(typeof(this.popupTemplate) !== 'undefined') { 136 | html = this.popupTemplate.call(this, this.model); 137 | } else if(typeof(this.fields) !== 'undefined') { 138 | if(typeof(this.title()) !== 'undefined' && this.title() !== '') { 139 | html += "

" + this.title() + "

"; 140 | } 141 | for(var i = 0; i < this.fields.length; i++) { 142 | html += "

" + this.fields[i] + ": " + this.get(this.fields[i]) + "

"; 143 | } 144 | } else { 145 | return ''; 146 | } 147 | return "
" + html + "
" 148 | } 149 | }; 150 | 151 | /* 152 | 153 | Providers only need respond to initialize and drawPoints 154 | 155 | */ 156 | 157 | Mapsheet.Providers = {}; 158 | 159 | 160 | /* 161 | 162 | Google Maps 163 | 164 | */ 165 | 166 | Mapsheet.Providers.Google = function(options) { 167 | this.map = options.map; 168 | this.mapOptions = merge_options( { mapTypeId: google.maps.MapTypeId.ROADMAP }, options.mapOptions || {} ); 169 | // We'll be nice and allow center to be a lat/lng array instead of a Google Maps LatLng 170 | if(this.mapOptions.center && this.mapOptions.center.length == 2) { 171 | this.mapOptions.center = new google.maps.LatLng(this.mapOptions.center[0], this.mapOptions.center[1]); 172 | } 173 | }; 174 | 175 | Mapsheet.Providers.Google.prototype = { 176 | initialize: function(element) { 177 | if(typeof(this.map) === 'undefined') { 178 | this.map = new google.maps.Map(element, this.mapOptions); 179 | } 180 | this.bounds = new google.maps.LatLngBounds(); 181 | this.infowindow = new google.maps.InfoWindow({ content: "loading...", maxWidth: '300' }); 182 | }, 183 | 184 | /* 185 | Google Maps only colors markers #FE7569, but turns out you can use 186 | the Google Charts API to make markers any hex color! Amazing. 187 | 188 | This code was pulled from 189 | http://stackoverflow.com/questions/7095574/google-maps-api-3-custom-marker-color-for-default-dot-marker/7686977#7686977 190 | */ 191 | 192 | setMarkerIcon: function(marker) { 193 | if(typeof(marker.point.get('icon url')) !== 'undefined' && marker.point.get('icon url') !== '') { 194 | marker.setIcon(marker.point.get('icon url')); 195 | return; 196 | }; 197 | 198 | if(typeof(marker.point.markerOptions['iconUrl']) !== 'undefined' && marker.point.markerOptions['iconUrl'] !== '') { 199 | marker.setIcon( marker.point.markerOptions['iconUrl']); 200 | return; 201 | }; 202 | 203 | var pinColor = marker.point.get('hexcolor') || "FE7569"; 204 | pinColor = pinColor.replace('#',''); 205 | 206 | var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=|" + pinColor, 207 | new google.maps.Size(21, 34), 208 | new google.maps.Point(0,0), 209 | new google.maps.Point(10, 34)); 210 | var pinShadow = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_shadow", 211 | new google.maps.Size(40, 37), 212 | new google.maps.Point(0, 0), 213 | new google.maps.Point(12, 35)); 214 | marker.setShadow(pinShadow); 215 | marker.setIcon(pinImage); 216 | }, 217 | 218 | drawMarker: function(point) { 219 | var latLng = new google.maps.LatLng(point.latitude(), point.longitude()); 220 | 221 | var marker = new google.maps.Marker({ 222 | position: latLng, 223 | point: point, 224 | title: point.title() 225 | }); 226 | 227 | this.setMarkerIcon(marker); 228 | this.initInfoWindow(marker); 229 | 230 | if(point.click) { 231 | google.maps.event.addListener(marker, 'click', function(e) { 232 | point.click.call(this, e, point); 233 | }); 234 | } 235 | 236 | return marker; 237 | }, 238 | 239 | /* 240 | Google only lets you draw one InfoWindow on the page, so you 241 | end up having to re-write to the original one each time. 242 | */ 243 | 244 | initInfoWindow: function(marker) { 245 | var infowindow = this.infowindow; 246 | var clickedOpen = false; 247 | 248 | // All of the extra blah blahs are for making sure to not repopulate 249 | // the infowindow when it's already opened and populated with the 250 | // right content 251 | 252 | google.maps.event.addListener(marker, 'click', function() { 253 | if(infowindow.getAnchor() === marker && infowindow.opened) { 254 | return; 255 | } 256 | infowindow.setContent(this.point.content()); 257 | infowindow.open(this.map, this); 258 | clickedOpen = true; 259 | infowindow.opened = true; 260 | }); 261 | 262 | }, 263 | 264 | drawPoints: function(points) { 265 | for(var i = 0; i < points.length; i++) { 266 | if(!points[i].isValid()) { continue; } 267 | var marker = this.drawMarker(points[i]); 268 | marker.setMap(this.map); 269 | this.bounds.extend(marker.position); 270 | points[i].marker = marker; 271 | }; 272 | 273 | if(!this.mapOptions.zoom && !this.mapOptions.center) { 274 | this.map.fitBounds(this.bounds); 275 | } 276 | } 277 | } 278 | 279 | /* 280 | 281 | MapQuest (OpenStreetMaps & free) 282 | 283 | */ 284 | 285 | Mapsheet.Providers.MapQuest = function(options) { 286 | this.map = options.map; 287 | this.mapOptions = merge_options({ mapTypeId: 'osm', zoom: 13, bestFitMargin: 0, zoomOnDoubleClick: true, latLng:{lat:40.735383, lng:-73.984655} }, options.mapOptions || {}); 288 | }; 289 | 290 | Mapsheet.Providers.MapQuest.prototype = { 291 | initialize: function(element) { 292 | if(typeof(this.map) === 'undefined') { 293 | this.map = new MQA.TileMap( merge_options({ elt: element }, this.mapOptions) ); 294 | } 295 | }, 296 | 297 | // We need custom icons! 298 | 299 | drawMarker: function(point) { 300 | var marker = new MQA.Poi( { lat: point.latitude(), lng: point.longitude() } ); 301 | 302 | marker.setRolloverContent(point.title()); 303 | marker.setInfoContentHTML(point.content()); 304 | 305 | if(point.click) { 306 | MQA.EventManager.addListener(marker, 'click', function(e) { 307 | point.click.call(this, e, point); 308 | }); 309 | } 310 | 311 | return marker; 312 | }, 313 | 314 | drawPoints: function(points) { 315 | for(var i = 0; i < points.length; i++) { 316 | if(!points[i].isValid()) { continue; } 317 | var marker = this.drawMarker(points[i]); 318 | this.map.addShape(marker); 319 | points[i].marker = marker; 320 | }; 321 | this.map.bestFit(); 322 | } 323 | } 324 | 325 | /* 326 | 327 | MapBox 328 | 329 | */ 330 | 331 | Mapsheet.Providers.MapBox = function(options) { 332 | this.map = options.map; 333 | this.mapOptions = merge_options({ mapId: 'examples.map-vyofok3q'}, options.mapOptions || {}); 334 | this.markerLayer = options.markerLayer || L.mapbox.markerLayer(); 335 | this.bounds = new L.LatLngBounds(); 336 | }; 337 | 338 | Mapsheet.Providers.MapBox.prototype = { 339 | initialize: function(element) { 340 | if(typeof(this.map) === 'undefined') { 341 | this.map = L.mapbox.map( element ); 342 | this.map.addLayer(L.mapbox.tileLayer(this.mapOptions['mapId'])); // add the base layer 343 | // this.map.ui.zoomer.add(); 344 | // this.map.ui.zoombox.add(); 345 | } 346 | }, 347 | 348 | drawMarker: function(point) { 349 | var marker = L.marker(point.coords()) 350 | .bindPopup(point.content()) 351 | 352 | if(typeof(point.get('icon url')) !== 'undefined' && point.get('icon url') !== '') { 353 | var options = merge_options( point.markerOptions, { iconUrl: point.get('icon url') } ); 354 | var icon = L.icon(options); 355 | marker.setIcon(icon); 356 | } else if(typeof(point.markerOptions['iconUrl']) !== 'undefined') { 357 | var icon = L.icon(point.markerOptions); 358 | marker.setIcon(icon); 359 | } 360 | 361 | if(point.click) { 362 | marker.on('click', function(e) { 363 | point.click.call(this, e, point); 364 | }); 365 | } 366 | // var icon = L.icon(); 367 | // marker.setIcon(icon); 368 | 369 | return marker; 370 | }, 371 | 372 | drawPoints: function(points) { 373 | for(var i = 0; i < points.length; i++) { 374 | if(!points[i].isValid()) { continue; } 375 | var marker = this.drawMarker(points[i]); 376 | marker.addTo(this.markerLayer); 377 | this.bounds.extend(marker.getLatLng()); 378 | points[i].marker = marker; 379 | }; 380 | 381 | this.markerLayer.addTo(this.map); 382 | 383 | if(!this.mapOptions.zoom && !this.mapOptions.center) { 384 | this.map.fitBounds(this.bounds); 385 | } 386 | } 387 | } 388 | 389 | /* 390 | 391 | Did you know you can pass in your own map? 392 | Check out https://gist.github.com/1804938 for some tips on using different tile providers 393 | 394 | */ 395 | 396 | Mapsheet.Providers.Leaflet = function(options) { 397 | this.map = options.map; 398 | 399 | var attribution = 'Map data © OpenStreetMap contributors, CC-BY-SA, tiles © MapQuest '; 400 | 401 | var layerDefaults = { 402 | styleId: 998, 403 | attribution: attribution, 404 | type: 'osm' 405 | }; 406 | 407 | this.layerOptions = merge_options(layerDefaults, options.layerOptions || {}); 408 | 409 | // Only overwrite if there's no tilePath, because the default subdomains is 'abc' 410 | if(!this.layerOptions.tilePath) { 411 | this.layerOptions.tilePath = 'http://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.png'; 412 | this.layerOptions.subdomains = '1234'; 413 | this.layerOptions.type = 'osm'; 414 | } 415 | this.markerLayer = options.markerLayer || new L.LayerGroup(); 416 | this.mapOptions = options.mapOptions || {}; 417 | this.bounds = new L.LatLngBounds(); 418 | }; 419 | 420 | Mapsheet.Providers.Leaflet.prototype = { 421 | initialize: function(element) { 422 | if(typeof(this.map) === 'undefined') { 423 | this.map = new L.Map('map', this.mapOptions); 424 | this.tileLayer = new L.TileLayer(this.layerOptions['tilePath'], this.layerOptions).addTo(this.map); 425 | } 426 | }, 427 | 428 | drawMarker: function(point) { 429 | var marker = L.marker(point.coords()) 430 | .bindPopup(point.content()) 431 | 432 | if(typeof(point.get('icon url')) !== 'undefined' && point.get('icon url') !== '') { 433 | var options = merge_options( point.markerOptions, { iconUrl: point.get('icon url') } ); 434 | var icon = L.icon(options); 435 | marker.setIcon(icon); 436 | } else if(typeof(point.markerOptions['iconUrl']) !== 'undefined') { 437 | var icon = L.icon(point.markerOptions); 438 | marker.setIcon(icon); 439 | } 440 | 441 | if(point.click) { 442 | marker.on('click', function(e) { 443 | point.click.call(this, e, point); 444 | }); 445 | } 446 | // var icon = L.icon(); 447 | // marker.setIcon(icon); 448 | 449 | return marker; 450 | }, 451 | 452 | drawPoints: function(points) { 453 | for(var i = 0; i < points.length; i++) { 454 | if(!points[i].isValid()) { continue; } 455 | var marker = this.drawMarker(points[i]); 456 | marker.addTo(this.markerLayer); 457 | this.bounds.extend(marker.getLatLng()); 458 | points[i].marker = marker; 459 | }; 460 | 461 | this.markerLayer.addTo(this.map); 462 | 463 | if(!this.mapOptions.zoom && !this.mapOptions.center) { 464 | this.map.fitBounds(this.bounds); 465 | } 466 | } 467 | } 468 | 469 | })(this); --------------------------------------------------------------------------------